diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 842d61ff1c..14e1ac5e44 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -99,6 +99,7 @@ jobs: Users, Webhooks, VCS, + Messaging, ] steps: diff --git a/.gitmodules b/.gitmodules index ff2e0a6aab..af12124355 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "app/console"] path = app/console url = https://github.com/appwrite/console - branch = 3.2.6 + branch = 3.2.7 diff --git a/CHANGES.md b/CHANGES.md index e8f0c08c1f..889f65e1e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,17 @@ +# Version 1.4.12 + +## Miscellaneous +* Bump console to version 3.2.7 [#7148](https://github.com/appwrite/appwrite/pull/7148) +* Chore update database to 0.45.2 [#7138](https://github.com/appwrite/appwrite/pull/7138) +* Implement queue thresholds for the health API [#7123](https://github.com/appwrite/appwrite/pull/7123) +* Add Authorization::skip to the usage worker [#7124](https://github.com/appwrite/appwrite/pull/7124) + +## Bug fixes +* fix: use queueForDeletes in git installation delete endpoint [#7140](https://github.com/appwrite/appwrite/pull/7140) +* fix: patch script, make errors silent [#7134](https://github.com/appwrite/appwrite/pull/7134) +* fix: repositories recreation script [#7133](https://github.com/appwrite/appwrite/pull/7133) +* fix: Only delete repositories linked to the particular project [#7131](https://github.com/appwrite/appwrite/pull/7131) + # Version 1.4.11 ## Miscellaneous diff --git a/Dockerfile b/Dockerfile index 33b1434659..059c499bd9 100755 --- a/Dockerfile +++ b/Dockerfile @@ -100,6 +100,7 @@ RUN chmod +x /usr/local/bin/doctor && \ RUN chmod +x /usr/local/bin/hamster && \ chmod +x /usr/local/bin/volume-sync && \ chmod +x /usr/local/bin/patch-delete-schedule-updated-at-attribute && \ + chmod +x /usr/local/bin/patch-recreate-repositories-documents && \ chmod +x /usr/local/bin/patch-delete-project-collections && \ chmod +x /usr/local/bin/delete-orphaned-projects && \ chmod +x /usr/local/bin/clear-card-cache && \ diff --git a/README-CN.md b/README-CN.md index 4e45c423c4..8e4695e608 100644 --- a/README-CN.md +++ b/README-CN.md @@ -55,7 +55,7 @@ Appwrite 可以提供给开发者用户验证,外部授权,用户数据读 ## 安装 -Appwrite 的容器化服务器只需要一行指令就可以运行。您可以使用 docker-compose 在本地主机上运行 Appwrite,也可以在任何其他容器化工具(如 Kubernetes、Docker Swarm 或 Rancher)上运行 Appwrite。 +Appwrite 的容器化服务器只需要一行指令就可以运行。您可以使用 docker-compose 在本地主机上运行 Appwrite,也可以在任何其他容器化工具(如 [Kubernetes](https://kubernetes.io/docs/home/)、[Docker Swarm](https://docs.docker.com/engine/swarm/) 或 [Rancher](https://rancher.com/docs/))上运行 Appwrite。 启动 Appwrite 服务器的最简单方法是运行我们的 docker-compose 文件。在运行安装命令之前,请确保您的机器上安装了 [Docker](https://dockerdocs.cn/get-docker/index.html): @@ -66,7 +66,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:1.4.11 + appwrite/appwrite:1.4.12 ``` ### Windows @@ -78,7 +78,7 @@ docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --entrypoint="install" ^ - appwrite/appwrite:1.4.11 + appwrite/appwrite:1.4.12 ``` #### PowerShell @@ -88,7 +88,7 @@ docker run -it --rm ` --volume /var/run/docker.sock:/var/run/docker.sock ` --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ` --entrypoint="install" ` - appwrite/appwrite:1.4.11 + appwrite/appwrite:1.4.12 ``` 运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。 diff --git a/README.md b/README.md index 88615a355a..3a1d2dbe9f 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:1.4.11 + appwrite/appwrite:1.4.12 ``` ### Windows @@ -88,7 +88,7 @@ docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --entrypoint="install" ^ - appwrite/appwrite:1.4.11 + appwrite/appwrite:1.4.12 ``` #### PowerShell @@ -98,7 +98,7 @@ docker run -it --rm ` --volume /var/run/docker.sock:/var/run/docker.sock ` --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ` --entrypoint="install" ` - appwrite/appwrite:1.4.11 + appwrite/appwrite:1.4.12 ``` Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation. diff --git a/app/config/collections.php b/app/config/collections.php index a1310eaa1d..614a805d35 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1416,17 +1416,6 @@ $commonCollections = [ 'array' => false, 'filters' => [], ], - [ - '$id' => ID::custom('internal'), - 'type' => Database::VAR_BOOLEAN, - 'signed' => true, - 'size' => 0, - 'format' => '', - 'filters' => [], - 'required' => false, - 'default' => false, - 'array' => false, - ], [ '$id' => ID::custom('enabled'), 'type' => Database::VAR_BOOLEAN, @@ -1467,9 +1456,9 @@ $commonCollections = [ 'size' => 65535, 'signed' => true, 'required' => false, - 'default' => null, + 'default' => '', 'array' => false, - 'filters' => [], + 'filters' => ['providerSearch'], ], ], 'indexes' => [ @@ -1495,16 +1484,9 @@ $commonCollections = [ 'orders' => [Database::ORDER_ASC], ], [ - '$id' => ID::custom('_key_internal'), + '$id' => ID::custom('_key_enabled_type'), 'type' => Database::INDEX_KEY, - 'attributes' => ['internal'], - 'lengths' => [], - 'orders' => [Database::ORDER_ASC], - ], - [ - '$id' => ID::custom('_key_internal_type'), - 'type' => Database::INDEX_KEY, - 'attributes' => ['internal','type'], + 'attributes' => ['enabled','type'], 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], @@ -1640,9 +1622,9 @@ $commonCollections = [ 'size' => 16384, 'signed' => true, 'required' => false, - 'default' => null, + 'default' => '', 'array' => false, - 'filters' => [], + 'filters' => ['messageSearch'], ], ], 'indexes' => [ @@ -1712,9 +1694,9 @@ $commonCollections = [ 'size' => 16384, 'signed' => true, 'required' => false, - 'default' => null, + 'default' => '', 'array' => false, - 'filters' => [], + 'filters' => ['topicSearch'], ], ], 'indexes' => [ @@ -1740,6 +1722,28 @@ $commonCollections = [ '$id' => ID::custom('subscribers'), 'name' => 'Subscribers', 'attributes' => [ + [ + '$id' => ID::custom('userId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('userInternalId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('targetId'), 'type' => Database::VAR_STRING, @@ -1786,6 +1790,20 @@ $commonCollections = [ ], ], 'indexes' => [ + [ + '$id' => ID::custom('_key_userId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['userId'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => ID::custom('_key_userInternalId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['userInternalId'], + 'lengths' => [], + 'orders' => [], + ], [ '$id' => ID::custom('_key_targetId'), 'type' => Database::INDEX_KEY, @@ -1845,7 +1863,7 @@ $commonCollections = [ 'filters' => [], ], [ - '$id' => ID::custom('providerId'), + '$id' => ID::custom('providerType'), 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, @@ -1855,13 +1873,24 @@ $commonCollections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('providerId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('providerInternalId'), 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, 'signed' => true, - 'required' => true, + 'required' => false, 'default' => null, 'array' => false, 'filters' => [], @@ -1877,6 +1906,17 @@ $commonCollections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('name'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [ [ @@ -1909,18 +1949,11 @@ $commonCollections = [ ], [ '$id' => ID::custom('_key_identifier'), - 'type' => Database::INDEX_KEY, + 'type' => Database::INDEX_UNIQUE, 'attributes' => ['identifier'], 'lengths' => [], 'orders' => [], ], - [ - '$id' => ID::custom('_key_identifier_providerId'), - 'type' => Database::INDEX_UNIQUE, - 'attributes' => ['providerId', 'identifier'], - 'lengths' => [], - 'orders' => [], - ] ], ], ]; diff --git a/app/config/errors.php b/app/config/errors.php index 1117f20e48..f7c9e8e55f 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -240,6 +240,16 @@ return [ 'description' => 'OAuth2 provider returned some error.', 'code' => 424, ], + Exception::USER_EMAIL_ALREADY_VERIFIED => [ + 'name' => Exception::USER_EMAIL_ALREADY_VERIFIED, + 'description' => 'User email is already verified', + 'code' => 409, + ], + Exception::USER_PHONE_ALREADY_VERIFIED => [ + 'name' => Exception::USER_PHONE_ALREADY_VERIFIED, + 'description' => 'User phone is already verified', + 'code' => 409 + ], Exception::USER_TARGET_NOT_FOUND => [ 'name' => Exception::USER_TARGET_NOT_FOUND, 'description' => 'The target could not be found.', diff --git a/app/config/variables.php b/app/config/variables.php index 9d555bf013..6f49d4a5da 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -440,7 +440,7 @@ return [ 'variables' => [ [ 'name' => '_APP_SMS_PROVIDER', - 'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'.\n\nEnsure `[USER]` and `[SECRET]` are URL encoded if they contain any non-alphanumeric characters.\n\nAvailable providers are twilio, text-magic, telesign, msg91, and vonage.", + 'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'.\n\nEnsure `[USER]` and `[SECRET]` are URL encoded if they contain any non-alphanumeric characters.\n\nAvailable providers are twilio, textmagic, telesign, msg91, and vonage.", 'introduction' => '0.15.0', 'default' => '', 'required' => false, diff --git a/app/console b/app/console index f7c34a1b37..49d039ed07 160000 --- a/app/console +++ b/app/console @@ -1 +1 @@ -Subproject commit f7c34a1b37d53dd5f28c83b4e12a4e68fcd9b484 +Subproject commit 49d039ed07628155e7f56e2c997fcef90ecde267 diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 96c0483cca..eda29656fe 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -148,7 +148,20 @@ App::post('/v1/account') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$internalId'); - Authorization::skip(fn() => $dbForProject->createDocument('users', $user)); + $user = Authorization::skip(fn() => $dbForProject->createDocument('users', $user)); + $target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([ + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::user($userId)), + Permission::delete(Role::user($userId)), + ], + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'providerType' => 'email', + 'identifier' => $email, + ]))); + $user->setAttribute('targets', [$target]); + $dbForProject->deleteCachedDocument('users', $user->getId()); } catch (Duplicate) { throw new Exception(Exception::USER_ALREADY_EXISTS); } @@ -656,7 +669,18 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$internalId'); - Authorization::skip(fn() => $dbForProject->createDocument('users', $user)); + $userDoc = Authorization::skip(fn() => $dbForProject->createDocument('users', $user)); + $dbForProject->createDocument('targets', new Document([ + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::user($user->getId())), + Permission::delete(Role::user($user->getId())), + ], + 'userId' => $userDoc->getId(), + 'userInternalId' => $userDoc->getInternalId(), + 'providerType' => 'email', + 'identifier' => $email, + ])); } catch (Duplicate) { $failureRedirect(Exception::USER_ALREADY_EXISTS); } @@ -1236,11 +1260,7 @@ App::post('/v1/account/sessions/phone') ->inject('queueForMessaging') ->inject('locale') ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale) { - $provider = Authorization::skip(fn () => $dbForProject->findOne('providers', [ - Query::equal('internal', [true]), - Query::equal('type', ['sms']) - ])); - if ($provider === false || $provider->isEmpty()) { + if (empty(App::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -1326,31 +1346,18 @@ App::post('/v1/account/sessions/phone') $message = $message->setParam('{{token}}', $secret); $message = $message->render(); - $target = $dbForProject->findOne('targets', [ - Query::equal('identifier', [$phone]), - Query::equal('providerInternalId', [$provider->getInternalId()]) - ]); - if (!$target) { - $target = $dbForProject->createDocument('targets', new Document([ - 'userId' => $user->getId(), - 'userInternalId' => $user->getInternalId(), - 'providerId' => $provider->getId(), - 'providerInternalId' => $provider->getInternalId(), - 'identifier' => $phone, - ])); - } - - $messageDoc = $dbForProject->createDocument('messages', new Document([ + $messageDoc = new Document([ '$id' => $token->getId(), - 'targets' => [$target->getId()], 'data' => [ 'content' => $message, ], - ])); + ]); $queueForMessaging - ->setMessageId($messageDoc->getId()) + ->setMessage($messageDoc) + ->setRecipients([$phone]) + ->setProviderType('SMS') ->setProject($project) ->trigger(); @@ -1670,6 +1677,81 @@ App::post('/v1/account/jwt') ])]), Response::MODEL_JWT); }); +App::post('/v1/account/targets/push') + ->desc('Create Account\'s push target') + ->groups(['api', 'account']) + ->label('error', __DIR__ . '/../../views/general/error.phtml') + ->label('audits.event', 'target.create') + ->label('audits.resource', 'target/response.$id') + ->label('event', 'users.[userId].targets.[targetId].create') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'createPushTarget') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_TARGET) + ->label('docs', false) + ->param('targetId', '', new CustomId(), 'Target 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.') + ->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.') + ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)') + ->inject('queueForEvents') + ->inject('user') + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $targetId, string $providerId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { + $targetId = $targetId == 'unique()' ? ID::unique() : $targetId; + + $provider = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId)); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + + if ($user->isEmpty()) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + + if (!$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + + $detector = new Detector($request->getUserAgent()); + $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + + $device = $detector->getDevice(); + + try { + $target = $dbForProject->createDocument('targets', new Document([ + '$id' => $targetId, + '$permissions' => [ + Permission::read(Role::user($user->getId())), + Permission::update(Role::user($user->getId())), + ], + 'providerId' => $providerId ?? null, + 'providerInternalId' => $provider->getInternalId() ?? null, + 'providerType' => 'push', + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'identifier' => $identifier, + 'name' => "{$device['deviceBrand']} {$device['deviceModel']}" + ])); + } catch (Duplicate) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); + + $queueForEvents + ->setParam('userId', $user->getId()) + ->setParam('targetId', $target->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($target, Response::MODEL_TARGET); + }); + App::get('/v1/account') ->desc('Get account') ->groups(['api', 'account']) @@ -1995,6 +2077,7 @@ App::patch('/v1/account/email') throw new Exception(Exception::USER_INVALID_CREDENTIALS); } + $oldEmail = $user->getAttribute('email'); $email = \strtolower($email); // Makes sure this email is not already used in another identity @@ -2019,6 +2102,23 @@ App::patch('/v1/account/email') ->setAttribute('passwordUpdate', DateTime::now()); } + $target = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$email]), + ]); + + if ($target && !$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + + /** + * @var Document $oldTarget + */ + $oldTarget = $user->find('identifier', $oldEmail, 'targets'); + + if ($oldTarget !== false && !$oldTarget->isEmpty()) { + $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email)); + } + try { $user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user)); } catch (Duplicate) { @@ -2065,6 +2165,19 @@ App::patch('/v1/account/phone') throw new Exception(Exception::USER_INVALID_CREDENTIALS); } + $target = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$phone]), + ]); + + if ($target && !$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + + /** + * @var Document $oldTarget + */ + $oldTarget = $user->find('identifier', $user->getAttribute('phone'), 'targets'); + $user ->setAttribute('phone', $phone) ->setAttribute('phoneVerification', false) // After this user needs to confirm phone number again @@ -2078,6 +2191,10 @@ App::patch('/v1/account/phone') ->setAttribute('passwordUpdate', DateTime::now()); } + if ($oldTarget !== false && !$oldTarget->isEmpty()) { + $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone)); + } + try { $user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user)); } catch (Duplicate $th) { @@ -2688,6 +2805,10 @@ App::post('/v1/account/verification') throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); } + if ($user->getAttribute('emailVerification')) { + throw new Exception(Exception::USER_EMAIL_ALREADY_VERIFIED); + } + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -2900,11 +3021,7 @@ App::post('/v1/account/verification/phone') ->inject('project') ->inject('locale') ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale) { - $provider = Authorization::skip(fn () => $dbForProject->findOne('providers', [ - Query::equal('internal', [true]), - Query::equal('type', ['sms']) - ])); - if ($provider === false || $provider->isEmpty()) { + if (empty(App::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -2912,6 +3029,10 @@ App::post('/v1/account/verification/phone') throw new Exception(Exception::USER_PHONE_NOT_FOUND); } + if ($user->getAttribute('phoneVerification')) { + throw new Exception(Exception::USER_PHONE_ALREADY_VERIFIED); + } + $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); @@ -2950,31 +3071,17 @@ App::post('/v1/account/verification/phone') $message = $message->setParam('{{token}}', $secret); $message = $message->render(); - $target = $dbForProject->findOne('targets', [ - Query::equal('identifier', [$user->getAttribute('phone')]), - Query::equal('providerInternalId', [$provider->getInternalId()]) - ]); - - if (!$target) { - $target = $dbForProject->createDocument('targets', new Document([ - 'userId' => $user->getId(), - 'userInternalId' => $user->getInternalId(), - 'providerId' => $provider->getId(), - 'providerInternalId' => $provider->getInternalId(), - 'identifier' => $user->getAttribute('phone'), - ])); - } - - $messageDoc = $dbForProject->createDocument('messages', new Document([ + $messageDoc = new Document([ '$id' => $verification->getId(), - 'targets' => [$target->getId()], 'data' => [ 'content' => $message, ], - ])); + ]); $queueForMessaging - ->setMessageId($messageDoc->getId()) + ->setMessage($messageDoc) + ->setRecipients([$user->getAttribute('phone')]) + ->setProviderType('SMS') ->setProject($project) ->trigger(); @@ -3053,3 +3160,61 @@ App::put('/v1/account/verification/phone') $response->dynamic($verificationDocument, Response::MODEL_TOKEN); }); + +App::put('/v1/account/targets/:targetId/push') + ->desc('Update Account\'s push target') + ->groups(['api', 'account']) + ->label('error', __DIR__ . '/../../views/general/error.phtml') + ->label('audits.event', 'target.update') + ->label('audits.resource', 'target/response.$id') + ->label('event', 'users.[userId].targets.[targetId].update') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'updatePushTarget') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_TARGET) + ->label('docs', false) + ->param('targetId', '', new UID(), 'Target ID.') + ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)') + ->inject('queueForEvents') + ->inject('user') + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { + if ($user->isEmpty()) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + + if ($target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($user->getId() !== $target->getAttribute('userId')) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($identifier) { + $target->setAttribute('identifier', $identifier); + } + + $detector = new Detector($request->getUserAgent()); + $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + + $device = $detector->getDevice(); + + $target->setAttribute('name', "{$device['deviceBrand']} {$device['deviceModel']}"); + + $target = $dbForProject->updateDocument('targets', $target->getId(), $target); + $dbForProject->deleteCachedDocument('users', $user->getId()); + + $queueForEvents + ->setParam('userId', $user->getId()) + ->setParam('targetId', $target->getId()); + + $response + ->dynamic($target, Response::MODEL_TARGET); + }); diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index f47e3f8265..7b3ed5a59f 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -1571,7 +1571,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') - ->param('default', null, new DatetimeValidator(), 'Default value for the attribute in ISO 8601 format. Cannot be set when attribute is required.', true) + ->param('default', null, new DatetimeValidator(), 'Default value for the attribute in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.', true) ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForProject') diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 268acc0692..cbdbd3a1cb 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -242,12 +242,16 @@ App::post('/v1/functions') // Git connect logic if (!empty($providerRepositoryId)) { + $teamId = $project->getAttribute('teamId', ''); + $repository = $dbForConsole->createDocument('repositories', new Document([ '$id' => ID::unique(), '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), + 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')), ], 'installationId' => $installation->getId(), 'installationInternalId' => $installation->getInternalId(), diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index e0e26781cf..90e080d5fa 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -14,6 +14,7 @@ use Utopia\Registry\Registry; use Utopia\Storage\Device; use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; +use Utopia\Validator\Integer; use Utopia\Validator\Text; App::get('/v1/health') @@ -344,11 +345,20 @@ App::get('/v1/health/queue/webhooks') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE) + ->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true) ->inject('queue') ->inject('response') - ->action(function (Connection $queue, Response $response) { + ->action(function (int|string $threshold, Connection $queue, Response $response) { + $threshold = \intval($threshold); + $client = new Client(Event::WEBHOOK_QUEUE_NAME, $queue); - $response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE); + $size = $client->getQueueSize(); + + if ($size >= $threshold) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); + } + + $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); App::get('/v1/health/queue/logs') @@ -362,11 +372,20 @@ App::get('/v1/health/queue/logs') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE) + ->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true) ->inject('queue') ->inject('response') - ->action(function (Connection $queue, Response $response) { + ->action(function (int|string $threshold, Connection $queue, Response $response) { + $threshold = \intval($threshold); + $client = new Client(Event::AUDITS_QUEUE_NAME, $queue); - $response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE); + $size = $client->getQueueSize(); + + if ($size >= $threshold) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); + } + + $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); App::get('/v1/health/queue/certificates') @@ -380,11 +399,20 @@ App::get('/v1/health/queue/certificates') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE) + ->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true) ->inject('queue') ->inject('response') - ->action(function (Connection $queue, Response $response) { + ->action(function (int|string $threshold, Connection $queue, Response $response) { + $threshold = \intval($threshold); + $client = new Client(Event::CERTIFICATES_QUEUE_NAME, $queue); - $response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE); + $size = $client->getQueueSize(); + + if ($size >= $threshold) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); + } + + $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); App::get('/v1/health/queue/builds') @@ -398,11 +426,20 @@ App::get('/v1/health/queue/builds') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE) + ->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true) ->inject('queue') ->inject('response') - ->action(function (Connection $queue, Response $response) { + ->action(function (int|string $threshold, Connection $queue, Response $response) { + $threshold = \intval($threshold); + $client = new Client(Event::BUILDS_QUEUE_NAME, $queue); - $response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE); + $size = $client->getQueueSize(); + + if ($size >= $threshold) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); + } + + $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); App::get('/v1/health/queue/databases') @@ -417,11 +454,20 @@ App::get('/v1/health/queue/databases') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE) ->param('name', 'database_db_main', new Text(256), 'Queue name for which to check the queue size', true) + ->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true) ->inject('queue') ->inject('response') - ->action(function (string $name, Connection $queue, Response $response) { + ->action(function (string $name, int|string $threshold, Connection $queue, Response $response) { + $threshold = \intval($threshold); + $client = new Client($name, $queue); - $response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE); + $size = $client->getQueueSize(); + + if ($size >= $threshold) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); + } + + $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); App::get('/v1/health/queue/deletes') @@ -435,11 +481,20 @@ App::get('/v1/health/queue/deletes') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE) + ->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true) ->inject('queue') ->inject('response') - ->action(function (Connection $queue, Response $response) { + ->action(function (int|string $threshold, Connection $queue, Response $response) { + $threshold = \intval($threshold); + $client = new Client(Event::DELETE_QUEUE_NAME, $queue); - $response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE); + $size = $client->getQueueSize(); + + if ($size >= $threshold) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); + } + + $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); App::get('/v1/health/queue/mails') @@ -453,11 +508,20 @@ App::get('/v1/health/queue/mails') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE) + ->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true) ->inject('queue') ->inject('response') - ->action(function (Connection $queue, Response $response) { + ->action(function (int|string $threshold, Connection $queue, Response $response) { + $threshold = \intval($threshold); + $client = new Client(Event::MAILS_QUEUE_NAME, $queue); - $response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE); + $size = $client->getQueueSize(); + + if ($size >= $threshold) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); + } + + $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); App::get('/v1/health/queue/messaging') @@ -471,11 +535,20 @@ App::get('/v1/health/queue/messaging') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE) + ->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true) ->inject('queue') ->inject('response') - ->action(function (Connection $queue, Response $response) { + ->action(function (int|string $threshold, Connection $queue, Response $response) { + $threshold = \intval($threshold); + $client = new Client(Event::MESSAGING_QUEUE_NAME, $queue); - $response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE); + $size = $client->getQueueSize(); + + if ($size >= $threshold) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); + } + + $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); App::get('/v1/health/queue/migrations') @@ -489,11 +562,20 @@ App::get('/v1/health/queue/migrations') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE) + ->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true) ->inject('queue') ->inject('response') - ->action(function (Connection $queue, Response $response) { + ->action(function (int|string $threshold, Connection $queue, Response $response) { + $threshold = \intval($threshold); + $client = new Client(Event::MIGRATIONS_QUEUE_NAME, $queue); - $response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE); + $size = $client->getQueueSize(); + + if ($size >= $threshold) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); + } + + $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); App::get('/v1/health/queue/functions') @@ -507,11 +589,20 @@ App::get('/v1/health/queue/functions') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE) + ->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true) ->inject('queue') ->inject('response') - ->action(function (Connection $queue, Response $response) { + ->action(function (int|string $threshold, Connection $queue, Response $response) { + $threshold = \intval($threshold); + $client = new Client(Event::FUNCTIONS_QUEUE_NAME, $queue); - $response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE); + $size = $client->getQueueSize(); + + if ($size >= $threshold) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); + } + + $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); App::get('/v1/health/storage/local') diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 0f892ad98b..35ba97c1af 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -1,5 +1,6 @@ desc('Create Mailgun provider') ->groups(['api', 'messaging']) @@ -68,7 +71,6 @@ App::post('/v1/messaging/providers/mailgun') 'provider' => 'mailgun', 'type' => 'email', 'enabled' => $enabled, - 'search' => $providerId . ' ' . $name . ' ' . 'mailgun' . ' ' . 'email', 'credentials' => [ 'apiKey' => $apiKey, 'domain' => $domain, @@ -79,16 +81,6 @@ App::post('/v1/messaging/providers/mailgun') ] ]); - // Check if a internal provider exists, if not, set this one as internal - if ( - empty($dbForProject->findOne('providers', [ - Query::equal('internal', [true]), - Query::equal('type', ['email']) - ])) - ) { - $provider->setAttribute('internal', true); - } - try { $provider = $dbForProject->createDocument('providers', $provider); } catch (DuplicateException) { @@ -119,7 +111,7 @@ App::post('/v1/messaging/providers/sendgrid') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider 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.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Text(256), 'Sender email address.') + ->param('from', '', new Email(), 'Sender email address.') ->param('apiKey', '', new Text(0), 'Sendgrid API key.') ->param('enabled', true, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') @@ -133,7 +125,6 @@ App::post('/v1/messaging/providers/sendgrid') 'provider' => 'sendgrid', 'type' => 'email', 'enabled' => $enabled, - 'search' => $providerId . ' ' . $name . ' ' . 'sendgrid' . ' ' . 'email', 'credentials' => [ 'apiKey' => $apiKey, ], @@ -142,16 +133,6 @@ App::post('/v1/messaging/providers/sendgrid') ] ]); - // Check if a internal provider exists, if not, set this one as internal - if ( - empty($dbForProject->findOne('providers', [ - Query::equal('internal', [true]), - Query::equal('type', ['sms']) - ])) - ) { - $provider->setAttribute('internal', true); - } - try { $provider = $dbForProject->createDocument('providers', $provider); } catch (DuplicateException) { @@ -182,7 +163,7 @@ App::post('/v1/messaging/providers/msg91') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider 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.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Text(256), 'Sender number.') + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') ->param('senderId', '', new Text(0), 'Msg91 Sender ID.') ->param('authKey', '', new Text(0), 'Msg91 Auth Key.') ->param('enabled', true, new Boolean(), 'Set as enabled.', true) @@ -196,7 +177,6 @@ App::post('/v1/messaging/providers/msg91') 'name' => $name, 'provider' => 'msg91', 'type' => 'sms', - 'search' => $providerId . ' ' . $name . ' ' . 'msg91' . ' ' . 'sms', 'enabled' => $enabled, 'credentials' => [ 'senderId' => $senderId, @@ -207,16 +187,6 @@ App::post('/v1/messaging/providers/msg91') ] ]); - // Check if a internal provider exists, if not, set this one as internal - if ( - empty($dbForProject->findOne('providers', [ - Query::equal('internal', [true]), - Query::equal('type', ['sms']) - ])) - ) { - $provider->setAttribute('internal', true); - } - try { $provider = $dbForProject->createDocument('providers', $provider); } catch (DuplicateException) { @@ -247,7 +217,7 @@ App::post('/v1/messaging/providers/telesign') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider 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.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Text(256), 'Sender number.') + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') ->param('username', '', new Text(0), 'Telesign username.') ->param('password', '', new Text(0), 'Telesign password.') ->param('enabled', true, new Boolean(), 'Set as enabled.', true) @@ -261,7 +231,6 @@ App::post('/v1/messaging/providers/telesign') 'name' => $name, 'provider' => 'telesign', 'type' => 'sms', - 'search' => $providerId . ' ' . $name . ' ' . 'telesign' . ' ' . 'sms', 'enabled' => $enabled, 'credentials' => [ 'username' => $username, @@ -272,16 +241,6 @@ App::post('/v1/messaging/providers/telesign') ] ]); - // Check if a internal provider exists, if not, set this one as internal - if ( - empty($dbForProject->findOne('providers', [ - Query::equal('internal', [true]), - Query::equal('type', ['sms']) - ])) - ) { - $provider->setAttribute('internal', true); - } - try { $provider = $dbForProject->createDocument('providers', $provider); } catch (DuplicateException) { @@ -312,7 +271,7 @@ App::post('/v1/messaging/providers/textmagic') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider 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.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Text(256), 'Sender number.') + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') ->param('username', '', new Text(0), 'Textmagic username.') ->param('apiKey', '', new Text(0), 'Textmagic apiKey.') ->param('enabled', true, new Boolean(), 'Set as enabled.', true) @@ -324,9 +283,8 @@ App::post('/v1/messaging/providers/textmagic') $provider = new Document([ '$id' => $providerId, 'name' => $name, - 'provider' => 'text-magic', + 'provider' => 'textmagic', 'type' => 'sms', - 'search' => $providerId . ' ' . $name . ' ' . 'text-magic' . ' ' . 'sms', 'enabled' => $enabled, 'credentials' => [ 'username' => $username, @@ -337,16 +295,6 @@ App::post('/v1/messaging/providers/textmagic') ] ]); - // Check if a internal provider exists, if not, set this one as internal - if ( - empty($dbForProject->findOne('providers', [ - Query::equal('internal', [true]), - Query::equal('type', ['sms']) - ])) - ) { - $provider->setAttribute('internal', true); - } - try { $provider = $dbForProject->createDocument('providers', $provider); } catch (DuplicateException) { @@ -377,7 +325,7 @@ App::post('/v1/messaging/providers/twilio') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider 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.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Text(256), 'Sender number.') + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') ->param('accountSid', '', new Text(0), 'Twilio account secret ID.') ->param('authToken', '', new Text(0), 'Twilio authentication token.') ->param('enabled', true, new Boolean(), 'Set as enabled.', true) @@ -391,7 +339,6 @@ App::post('/v1/messaging/providers/twilio') 'name' => $name, 'provider' => 'twilio', 'type' => 'sms', - 'search' => $providerId . ' ' . $name . ' ' . 'twilio' . ' ' . 'sms', 'enabled' => $enabled, 'credentials' => [ 'accountSid' => $accountSid, @@ -402,16 +349,6 @@ App::post('/v1/messaging/providers/twilio') ] ]); - // Check if a internal provider exists, if not, set this one as internal - if ( - empty($dbForProject->findOne('providers', [ - Query::equal('internal', [true]), - Query::equal('type', ['sms']) - ])) - ) { - $provider->setAttribute('internal', true); - } - try { $provider = $dbForProject->createDocument('providers', $provider); } catch (DuplicateException) { @@ -442,7 +379,7 @@ App::post('/v1/messaging/providers/vonage') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider 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.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Text(256), 'Sender number.') + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') ->param('apiKey', '', new Text(0), 'Vonage API key.') ->param('apiSecret', '', new Text(0), 'Vonage API secret.') ->param('enabled', true, new Boolean(), 'Set as enabled.', true) @@ -456,7 +393,6 @@ App::post('/v1/messaging/providers/vonage') 'name' => $name, 'provider' => 'vonage', 'type' => 'sms', - 'search' => $providerId . ' ' . $name . ' ' . 'vonage' . ' ' . 'sms', 'enabled' => $enabled, 'credentials' => [ 'apiKey' => $apiKey, @@ -467,16 +403,6 @@ App::post('/v1/messaging/providers/vonage') ] ]); - // Check if a internal provider exists, if not, set this one as internal - if ( - empty($dbForProject->findOne('providers', [ - Query::equal('internal', [true]), - Query::equal('type', ['sms']) - ])) - ) { - $provider->setAttribute('internal', true); - } - try { $provider = $dbForProject->createDocument('providers', $provider); } catch (DuplicateException) { @@ -507,35 +433,24 @@ App::post('/v1/messaging/providers/fcm') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider 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.') ->param('name', '', new Text(128), 'Provider name.') - ->param('enabled', true, new Boolean(), 'Set as enabled.', true) ->param('serverKey', '', new Text(0), 'FCM server key.') + ->param('enabled', true, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, bool $enabled, string $serverKey, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $serverKey, bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; $provider = new Document([ '$id' => $providerId, 'name' => $name, 'provider' => 'fcm', 'type' => 'push', - 'search' => $providerId . ' ' . $name . ' ' . 'fcm' . ' ' . 'push', 'enabled' => $enabled, 'credentials' => [ 'serverKey' => $serverKey, ], ]); - // Check if a internal provider exists, if not, set this one as internal - if ( - empty($dbForProject->findOne('providers', [ - Query::equal('internal', [true]), - Query::equal('type', ['push']) - ])) - ) { - $provider->setAttribute('internal', true); - } - try { $provider = $dbForProject->createDocument('providers', $provider); } catch (DuplicateException) { @@ -566,23 +481,22 @@ App::post('/v1/messaging/providers/apns') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider 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.') ->param('name', '', new Text(128), 'Provider name.') - ->param('enabled', true, new Boolean(), 'Set as enabled.', true) ->param('authKey', '', new Text(0), 'APNS authentication key.') ->param('authKeyId', '', new Text(0), 'APNS authentication key ID.') ->param('teamId', '', new Text(0), 'APNS team ID.') ->param('bundleId', '', new Text(0), 'APNS bundle ID.') ->param('endpoint', '', new Text(0), 'APNS endpoint.') + ->param('enabled', true, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; $provider = new Document([ '$id' => $providerId, 'name' => $name, 'provider' => 'apns', 'type' => 'push', - 'search' => $providerId . ' ' . $name . ' ' . 'apns' . ' ' . 'push', 'enabled' => $enabled, 'credentials' => [ 'authKey' => $authKey, @@ -593,16 +507,6 @@ App::post('/v1/messaging/providers/apns') ], ]); - // Check if a internal provider exists, if not, set this one as internal - if ( - empty($dbForProject->findOne('providers', [ - Query::equal('internal', [true]), - Query::equal('type', ['push']) - ])) - ) { - $provider->setAttribute('internal', true); - } - try { $provider = $dbForProject->createDocument('providers', $provider); } catch (DuplicateException) { @@ -784,15 +688,14 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) - ->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', true) - ->param('isEuRegion', null, new Boolean(), 'Set as eu region.', true) + ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true) ->param('from', '', new Text(256), 'Sender email address.', true) ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true) ->param('domain', '', new Text(0), 'Mailgun Domain.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $internal, ?bool $isEuRegion, string $from, string $apiKey, string $domain, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $isEuRegion, string $from, string $apiKey, string $domain, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -806,7 +709,6 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') if (!empty($name)) { $provider->setAttribute('name', $name); - $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'mailgun' . ' ' . 'email'); } if (!empty($from)) { @@ -819,10 +721,6 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') $provider->setAttribute('enabled', $enabled); } - if ($internal === true) { - $provider->setAttribute('internal', $internal); - } - $credentials = $provider->getAttribute('credentials'); if ($isEuRegion === true || $isEuRegion === false) { @@ -841,15 +739,6 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); - if ($internal === true) { - $internalProvider = $dbForProject->findOne('providers', [ - 'internal' => true, - 'type' => 'email', - ]); - $internalProvider->setAttribute('internal', false); - $dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider); - } - $queueForEvents ->setParam('providerId', $provider->getId()); @@ -874,13 +763,12 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) - ->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', true) ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true) ->param('from', '', new Text(256), 'Sender email address.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $internal, string $apiKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -894,7 +782,6 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') if (!empty($name)) { $provider->setAttribute('name', $name); - $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'sendgrid' . ' ' . 'email'); } if (!empty($from)) { @@ -907,10 +794,6 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') $provider->setAttribute('enabled', $enabled); } - if ($internal === true) { - $provider->setAttribute('internal', $internal); - } - if (!empty($apiKey)) { $provider->setAttribute('credentials', [ 'apiKey' => $apiKey, @@ -919,15 +802,6 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); - if ($internal === true) { - $internalProvider = $dbForProject->findOne('providers', [ - 'internal' => true, - 'type' => 'email', - ]); - $internalProvider->setAttribute('internal', false); - $dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider); - } - $queueForEvents ->setParam('providerId', $provider->getId()); @@ -952,14 +826,13 @@ App::patch('/v1/messaging/providers/msg91/:providerId') ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) - ->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', true) ->param('senderId', '', new Text(0), 'Msg91 Sender ID.', true) ->param('authKey', '', new Text(0), 'Msg91 Auth Key.', true) ->param('from', '', new Text(256), 'Sender number.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $internal, string $senderId, string $authKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, string $senderId, string $authKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -973,7 +846,6 @@ App::patch('/v1/messaging/providers/msg91/:providerId') if (!empty($name)) { $provider->setAttribute('name', $name); - $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'msg91' . ' ' . 'sms'); } if (!empty($from)) { @@ -986,10 +858,6 @@ App::patch('/v1/messaging/providers/msg91/:providerId') $provider->setAttribute('enabled', $enabled); } - if ($internal === true) { - $provider->setAttribute('internal', $internal); - } - $credentials = $provider->getAttribute('credentials'); if (!empty($senderId)) { @@ -1004,15 +872,6 @@ App::patch('/v1/messaging/providers/msg91/:providerId') $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); - if ($internal === true) { - $internalProvider = $dbForProject->findOne('providers', [ - 'internal' => true, - 'type' => 'email', - ]); - $internalProvider->setAttribute('internal', false); - $dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider); - } - $queueForEvents ->setParam('providerId', $provider->getId()); @@ -1037,14 +896,13 @@ App::patch('/v1/messaging/providers/telesign/:providerId') ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) - ->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', true) ->param('username', '', new Text(0), 'Telesign username.', true) ->param('password', '', new Text(0), 'Telesign password.', true) ->param('from', '', new Text(256), 'Sender number.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $internal, string $username, string $password, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, string $username, string $password, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -1058,7 +916,6 @@ App::patch('/v1/messaging/providers/telesign/:providerId') if (!empty($name)) { $provider->setAttribute('name', $name); - $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'telesign' . ' ' . 'sms'); } if (!empty($from)) { @@ -1071,10 +928,6 @@ App::patch('/v1/messaging/providers/telesign/:providerId') $provider->setAttribute('enabled', $enabled); } - if ($internal === true) { - $provider->setAttribute('internal', $internal); - } - $credentials = $provider->getAttribute('credentials'); if (!empty($username)) { @@ -1089,15 +942,6 @@ App::patch('/v1/messaging/providers/telesign/:providerId') $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); - if ($internal === true) { - $internalProvider = $dbForProject->findOne('providers', [ - 'internal' => true, - 'type' => 'email', - ]); - $internalProvider->setAttribute('internal', false); - $dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider); - } - $queueForEvents ->setParam('providerId', $provider->getId()); @@ -1122,14 +966,13 @@ App::patch('/v1/messaging/providers/textmagic/:providerId') ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) - ->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', true) ->param('username', '', new Text(0), 'Textmagic username.', true) ->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true) ->param('from', '', new Text(256), 'Sender number.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $internal, string $username, string $apiKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, string $username, string $apiKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -1137,13 +980,12 @@ App::patch('/v1/messaging/providers/textmagic/:providerId') } $providerAttr = $provider->getAttribute('provider'); - if ($providerAttr !== 'text-magic') { + if ($providerAttr !== 'textmagic') { throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); } if (!empty($name)) { $provider->setAttribute('name', $name); - $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'textmagic' . ' ' . 'sms'); } if (!empty($from)) { @@ -1156,10 +998,6 @@ App::patch('/v1/messaging/providers/textmagic/:providerId') $provider->setAttribute('enabled', $enabled); } - if ($internal === true) { - $provider->setAttribute('internal', $internal); - } - $credentials = $provider->getAttribute('credentials'); if (!empty($username)) { @@ -1174,15 +1012,6 @@ App::patch('/v1/messaging/providers/textmagic/:providerId') $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); - if ($internal === true) { - $internalProvider = $dbForProject->findOne('providers', [ - 'internal' => true, - 'type' => 'email', - ]); - $internalProvider->setAttribute('internal', false); - $dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider); - } - $queueForEvents ->setParam('providerId', $provider->getId()); @@ -1207,14 +1036,13 @@ App::patch('/v1/messaging/providers/twilio/:providerId') ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) - ->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', true) ->param('accountSid', null, new Text(0), 'Twilio account secret ID.', true) ->param('authToken', null, new Text(0), 'Twilio authentication token.', true) ->param('from', '', new Text(256), 'Sender number.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $internal, string $accountSid, string $authToken, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, string $accountSid, string $authToken, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -1228,7 +1056,6 @@ App::patch('/v1/messaging/providers/twilio/:providerId') if (!empty($name)) { $provider->setAttribute('name', $name); - $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'twilio' . ' ' . 'sms'); } if (!empty($from)) { @@ -1241,10 +1068,6 @@ App::patch('/v1/messaging/providers/twilio/:providerId') $provider->setAttribute('enabled', $enabled); } - if ($internal === true) { - $provider->setAttribute('internal', $internal); - } - $credentials = $provider->getAttribute('credentials'); if (!empty($accountSid)) { @@ -1259,15 +1082,6 @@ App::patch('/v1/messaging/providers/twilio/:providerId') $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); - if ($internal === true) { - $internalProvider = $dbForProject->findOne('providers', [ - 'internal' => true, - 'type' => 'email', - ]); - $internalProvider->setAttribute('internal', false); - $dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider); - } - $queueForEvents ->setParam('providerId', $provider->getId()); @@ -1292,14 +1106,13 @@ App::patch('/v1/messaging/providers/vonage/:providerId') ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) - ->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', true) ->param('apiKey', '', new Text(0), 'Vonage API key.', true) ->param('apiSecret', '', new Text(0), 'Vonage API secret.', true) ->param('from', '', new Text(256), 'Sender number.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $internal, string $apiKey, string $apiSecret, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $apiSecret, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -1313,7 +1126,6 @@ App::patch('/v1/messaging/providers/vonage/:providerId') if (!empty($name)) { $provider->setAttribute('name', $name); - $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'vonage' . ' ' . 'sms'); } if (!empty($from)) { @@ -1326,10 +1138,6 @@ App::patch('/v1/messaging/providers/vonage/:providerId') $provider->setAttribute('enabled', $enabled); } - if ($internal === true) { - $provider->setAttribute('internal', $internal); - } - $credentials = $provider->getAttribute('credentials'); if (!empty($apiKey)) { @@ -1344,15 +1152,6 @@ App::patch('/v1/messaging/providers/vonage/:providerId') $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); - if ($internal === true) { - $internalProvider = $dbForProject->findOne('providers', [ - 'internal' => true, - 'type' => 'email', - ]); - $internalProvider->setAttribute('internal', false); - $dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider); - } - $queueForEvents ->setParam('providerId', $provider->getId()); @@ -1377,12 +1176,11 @@ App::patch('/v1/messaging/providers/fcm/:providerId') ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) - ->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', true) ->param('serverKey', '', new Text(0), 'FCM Server Key.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $internal, string $serverKey, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, string $serverKey, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -1396,32 +1194,18 @@ App::patch('/v1/messaging/providers/fcm/:providerId') if (!empty($name)) { $provider->setAttribute('name', $name); - $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'fcm' . ' ' . 'push'); } if ($enabled === true || $enabled === false) { $provider->setAttribute('enabled', $enabled); } - if ($internal === true) { - $provider->setAttribute('internal', $internal); - } - if (!empty($serverKey)) { $provider->setAttribute('credentials', ['serverKey' => $serverKey]); } $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); - if ($internal === true) { - $internalProvider = $dbForProject->findOne('providers', [ - 'internal' => true, - 'type' => 'email', - ]); - $internalProvider->setAttribute('internal', false); - $dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider); - } - $queueForEvents ->setParam('providerId', $provider->getId()); @@ -1447,7 +1231,6 @@ App::patch('/v1/messaging/providers/apns/:providerId') ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) - ->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', true) ->param('authKey', '', new Text(0), 'APNS authentication key.', true) ->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true) ->param('teamId', '', new Text(0), 'APNS team ID.', true) @@ -1456,7 +1239,7 @@ App::patch('/v1/messaging/providers/apns/:providerId') ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $internal, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -1470,17 +1253,12 @@ App::patch('/v1/messaging/providers/apns/:providerId') if (!empty($name)) { $provider->setAttribute('name', $name); - $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'apns' . ' ' . 'push'); } if ($enabled === true || $enabled === false) { $provider->setAttribute('enabled', $enabled); } - if ($internal === true) { - $provider->setAttribute('internal', $internal); - } - $credentials = $provider->getAttribute('credentials'); if (!empty($authKey)) { @@ -1507,15 +1285,6 @@ App::patch('/v1/messaging/providers/apns/:providerId') $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); - if ($internal === true) { - $internalProvider = $dbForProject->findOne('providers', [ - 'internal' => true, - 'type' => 'email', - ]); - $internalProvider->setAttribute('internal', false); - $dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider); - } - $queueForEvents ->setParam('providerId', $provider->getId()); @@ -1588,9 +1357,6 @@ App::post('/v1/messaging/topics') if ($description) { $topic->setAttribute('description', $description); - $topic->setAttribute('search', $topic->getId() . ' ' . $name . ' ' . $description); - } else { - $topic->setAttribute('search', $topic->getId() . ' ' . $name); } try { @@ -1796,16 +1562,6 @@ App::patch('/v1/messaging/topics/:topicId') $topic->setAttribute('description', $description); } - if (!empty($name) || !empty($description)) { - if (!empty($name) && !empty($description)) { - $topic->setAttribute('search', $topic->getId() . ' ' . $name . ' ' . $description); - } elseif (!empty($name)) { - $topic->setAttribute('search', $topic->getId() . ' ' . $name . ' ' . $topic->getAttribute('description')); - } else { - $topic->setAttribute('search', $topic->getId() . ' ' . $topic->getAttribute('name') . ' ' . $description); - } - } - $topic = $dbForProject->updateDocument('topics', $topicId, $topic); $queueForEvents @@ -1856,7 +1612,7 @@ App::delete('/v1/messaging/topics/:topicId') }); App::post('/v1/messaging/topics/:topicId/subscribers') - ->desc('Adds a subscriber to a topic.') + ->desc('Create a subscriber.') ->groups(['api', 'messaging']) ->label('audits.event', 'subscriber.create') ->label('audits.resource', 'subscriber/{response.$id}') @@ -1869,9 +1625,9 @@ App::post('/v1/messaging/topics/:topicId/subscribers') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_SUBSCRIBER) - ->param('subscriberId', '', new CustomId(), 'Subscriber ID. Choose a custom Topic ID or a new Topic ID.') - ->param('topicId', '', new UID(), 'Topic ID.') - ->param('targetId', '', new UID(), 'Target ID.') + ->param('subscriberId', '', new CustomId(), 'Subscriber ID. Choose a custom Subscriber ID or a new Subscriber ID.') + ->param('topicId', '', new UID(), 'Topic ID. The topic ID to subscribe to.') + ->param('targetId', '', new UID(), 'Target ID. The target ID to link to the specified Topic ID.') ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') @@ -1890,12 +1646,16 @@ App::post('/v1/messaging/topics/:topicId/subscribers') throw new Exception(Exception::USER_TARGET_NOT_FOUND); } + $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); + $subscriber = new Document([ '$id' => $subscriberId, '$permissions' => [ - Permission::read(Role::user($target->getAttribute('userId'))), + Permission::read(Role::user($user->getId())), Permission::delete(Role::user($target->getAttribute('userId'))), ], + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), 'topicId' => $topicId, 'topicInternalId' => $topic->getInternalId(), 'targetId' => $targetId, @@ -1913,13 +1673,15 @@ App::post('/v1/messaging/topics/:topicId/subscribers') ->setParam('topicId', $topic->getId()) ->setParam('subscriberId', $subscriber->getId()); + $subscriber->setAttribute('userName', $user->getAttribute('name')); + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($subscriber, Response::MODEL_SUBSCRIBER); }); App::get('/v1/messaging/topics/:topicId/subscribers') - ->desc('List topic\'s subscribers.') + ->desc('List subscribers.') ->groups(['api', 'messaging']) ->label('scope', 'subscribers.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) @@ -1929,7 +1691,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_SUBSCRIBER_LIST) - ->param('topicId', '', new UID(), 'Topic ID.') + ->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.') ->param('queries', [], new Subscribers(), '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(', ', Providers::ALLOWED_ATTRIBUTES), true) ->inject('dbForProject') ->inject('response') @@ -1959,9 +1721,20 @@ App::get('/v1/messaging/topics/:topicId/subscribers') $cursor->setValue($cursorDocument); } + $subscribers = $dbForProject->find('subscribers', $queries); + + $subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject) { + return function () use ($subscriber, $dbForProject) { + $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $subscriber->getAttribute('userId'))); + + return $subscriber + ->setAttribute('userName', $user->getAttribute('name')); + }; + }, $subscribers)); + $response ->dynamic(new Document([ - 'subscribers' => $dbForProject->find('subscribers', $queries), + 'subscribers' => $subscribers, 'total' => $dbForProject->count('subscribers', $queries, APP_LIMIT_COUNT), ]), Response::MODEL_SUBSCRIBER_LIST); }); @@ -2050,8 +1823,8 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs') ]), Response::MODEL_LOG_LIST); }); -App::get('/v1/messaging/topics/:topicId/subscriber/:subscriberId') - ->desc('Get a topic\'s subscriber.') +App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') + ->desc('Get a subscriber.') ->groups(['api', 'messaging']) ->label('scope', 'subscribers.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) @@ -2061,7 +1834,7 @@ App::get('/v1/messaging/topics/:topicId/subscriber/:subscriberId') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_SUBSCRIBER) - ->param('topicId', '', new UID(), 'Topic ID.') + ->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.') ->param('subscriberId', '', new UID(), 'Subscriber ID.') ->inject('dbForProject') ->inject('response') @@ -2078,12 +1851,16 @@ App::get('/v1/messaging/topics/:topicId/subscriber/:subscriberId') throw new Exception(Exception::SUBSCRIBER_NOT_FOUND); } + $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $subscriber->getAttribute('userId'))); + + $subscriber->setAttribute('userName', $user->getAttribute('name')); + $response ->dynamic($subscriber, Response::MODEL_SUBSCRIBER); }); -App::delete('/v1/messaging/topics/:topicId/subscriber/:subscriberId') - ->desc('Delete a subscriber from a topic.') +App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') + ->desc('Delete a subscriber.') ->groups(['api', 'messaging']) ->label('audits.event', 'subscriber.delete') ->label('audits.resource', 'subscriber/{request.$subscriberId}') @@ -2096,7 +1873,7 @@ App::delete('/v1/messaging/topics/:topicId/subscriber/:subscriberId') ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_NONE) - ->param('topicId', '', new UID(), 'Topic ID.') + ->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.') ->param('subscriberId', '', new UID(), 'Subscriber ID.') ->inject('queueForEvents') ->inject('dbForProject') @@ -2149,7 +1926,7 @@ App::post('/v1/messaging/messages/email') ->param('description', '', new Text(256), 'Description for message.', true) ->param('status', 'processing', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true) ->param('html', false, new Boolean(), 'Is content of type HTML', true) - ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in ISO 8601 format. DateTime value must be in future.', true) + ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') @@ -2174,7 +1951,6 @@ App::post('/v1/messaging/messages/email') 'html' => $html, ], 'status' => $status, - 'search' => $messageId . ' ' . $description . ' ' . $subject, ])); if ($status === 'processing') { @@ -2213,7 +1989,7 @@ App::post('/v1/messaging/messages/sms') ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true) ->param('description', '', new Text(256), 'Description for Message.', true) ->param('status', 'processing', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true) - ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in ISO 8601 format. DateTime value must be in future.', true) + ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') @@ -2236,7 +2012,6 @@ App::post('/v1/messaging/messages/sms') 'content' => $content, ], 'status' => $status, - 'search' => $messageId . ' ' . $description, ])); if ($status === 'processing') { @@ -2283,7 +2058,7 @@ App::post('/v1/messaging/messages/push') ->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true) ->param('badge', '', new Text(256), 'Badge for push notification. Available only for IOS Platform.', true) ->param('status', 'processing', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true) - ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in ISO 8601 format. DateTime value must be in future.', true) + ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') @@ -2338,7 +2113,6 @@ App::post('/v1/messaging/messages/push') 'deliveryTime' => $deliveryTime, 'data' => $pushData, 'status' => $status, - 'search' => $messageId . ' ' . $description . ' ' . $title, ])); if ($status === 'processing') { @@ -2530,7 +2304,7 @@ App::patch('/v1/messaging/messages/email/:messageId') ->param('content', '', new Text(64230), 'Email Content.', true) ->param('status', '', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true) ->param('html', false, new Boolean(), 'Is content of type HTML', true) - ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in ISO 8601 format. DateTime value must be in future.', true) + ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') @@ -2583,8 +2357,6 @@ App::patch('/v1/messaging/messages/email/:messageId') $message->setAttribute('description', $description); } - $message->setAttribute('search', $message->getId() . ' ' . $message->getAttribute('description') . ' ' . $data['subject'] . ' ' . $message->getAttribute('providerId')); - if (!empty($status)) { $message->setAttribute('status', $status); } @@ -2630,7 +2402,7 @@ App::patch('/v1/messaging/messages/sms/:messageId') ->param('description', '', new Text(256), 'Description for Message.', true) ->param('content', '', new Text(64230), 'Email Content.', true) ->param('status', '', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true) - ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in ISO 8601 format. DateTime value must be in future.', true) + ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') @@ -2683,8 +2455,6 @@ App::patch('/v1/messaging/messages/sms/:messageId') $message->setAttribute('deliveryTime', $deliveryTime); } - $message->setAttribute('search', $message->getId() . ' ' . $message->getAttribute('description') . ' ' . $message->getAttribute('providerId')); - $message = $dbForProject->updateDocument('messages', $message->getId(), $message); if ($status === 'processing') { @@ -2729,7 +2499,7 @@ App::patch('/v1/messaging/messages/push/:messageId') ->param('color', '', new Text(256), 'Color for push notification. Available only for Android Platform.', true) ->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true) ->param('badge', '', new Text(256), 'Badge for push notification. Available only for IOS Platform.', true) ->param('status', 'processing', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true) - ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in ISO 8601 format. DateTime value must be in future.', true) + ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') @@ -2814,8 +2584,6 @@ App::patch('/v1/messaging/messages/push/:messageId') $message->setAttribute('deliveryTime', $deliveryTime); } - $message->setAttribute('search', $message->getId() . ' ' . $message->getAttribute('description') . ' ' . $pushData['title'] . ' ' . $message->getAttribute('providerId')); - $message = $dbForProject->updateDocument('messages', $message->getId(), $message); if ($status === 'processing') { diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 83e8fdb534..c03f7ce165 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1154,7 +1154,7 @@ App::post('/v1/projects/:projectId/keys') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('name', null, new Text(128), 'Key name. Max length: 128 chars.') ->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.') - ->param('expire', null, new DatetimeValidator(), 'Expiration time in ISO 8601 format. Use null for unlimited expiration.', true) + ->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true) ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) { @@ -1271,7 +1271,7 @@ App::put('/v1/projects/:projectId/keys/:keyId') ->param('keyId', '', new UID(), 'Key unique ID.') ->param('name', null, new Text(128), 'Key name. Max length: 128 chars.') ->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.') - ->param('expire', null, new DatetimeValidator(), 'Expiration time in ISO 8601 format. Use null for unlimited expiration.', true) + ->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true) ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) { diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index a4ad61733f..2ba27efcb3 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -628,12 +628,7 @@ App::post('/v1/teams/:teamId/memberships') ->trigger() ; } elseif (!empty($phone)) { - $provider = Authorization::skip(fn () => $dbForProject->findOne('providers', [ - Query::equal('internal', [true]), - Query::equal('type', ['sms']) - ])); - - if ($provider === false || $provider->isEmpty()) { + if (empty(App::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -647,28 +642,17 @@ App::post('/v1/teams/:teamId/memberships') $message = $message->setParam('{{token}}', $url); $message = $message->render(); - $target = $dbForProject->createDocument('targets', new Document([ - 'userId' => $invitee->getId(), - 'userInternalId' => $invitee->getInternalId(), - 'providerId' => $provider->getId(), - 'providerInternalId' => $provider->getInternalId(), - 'identifier' => $phone, - ])); - - $messageDoc = $dbForProject->createDocument('messages', new Document([ - // Here membership ID is used as message ID so that it can be used in test cases to verify the message - '$id' => $membership->getId(), - 'targets' => [$target->getId()], + $messageDoc = new Document([ + '$id' => ID::unique(), 'data' => [ 'content' => $message, ], - 'providerId' => $provider->getId(), - 'providerInternalId' => $provider->getInternalId(), - 'deliveryTime' => Datetime::now(), - ])); + ]); $queueForMessaging - ->setMessageId($messageDoc->getId()) + ->setMessage($messageDoc) + ->setRecipients([$phone]) + ->setProviderType('SMS') ->setProject($project) ->trigger(); } diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 71deded08f..d9968c83a7 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -394,18 +394,25 @@ App::post('/v1/users/:userId/targets') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_TARGET) + ->param('targetId', '', new CustomId(), 'Target 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.') ->param('userId', '', new UID(), 'User ID.') - ->param('targetId', '', new UID(), 'Target ID.') - ->param('providerId', '', new UID(), 'Provider ID.') + ->param('providerType', '', new WhiteList(['email', 'sms', 'push']), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.') ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)') + ->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true) ->inject('queueForEvents') ->inject('response') ->inject('dbForProject') - ->action(function (string $userId, string $targetId, string $providerId, string $identifier, Event $queueForEvents, Response $response, Database $dbForProject) { - $provider = $dbForProject->getDocument('providers', $providerId); + ->action(function (string $targetId, string $userId, string $providerType, string $identifier, string $providerId, Event $queueForEvents, Response $response, Database $dbForProject) { + $targetId = $targetId == 'unique()' ? ID::unique() : $targetId; - if ($provider->isEmpty()) { - throw new Exception(Exception::PROVIDER_NOT_FOUND); + $provider = new Document(); + + if ($providerType === 'push') { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } } $user = $dbForProject->getDocument('users', $userId); @@ -423,8 +430,9 @@ App::post('/v1/users/:userId/targets') try { $target = $dbForProject->createDocument('targets', new Document([ '$id' => $targetId, - 'providerId' => $providerId, - 'providerInternalId' => $provider->getInternalId(), + 'providerId' => $providerId ?? null, + 'providerInternalId' => $provider->getInternalId() ?? null, + 'providerType' => $providerType, 'userId' => $userId, 'userInternalId' => $user->getInternalId(), 'identifier' => $identifier, @@ -1223,8 +1231,8 @@ App::patch('/v1/users/:userId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); -App::patch('/v1/users/:userId/targets/:targetId/identifier') - ->desc('Update user target\'s identifier') +App::patch('/v1/users/:userId/targets/:targetId') + ->desc('Update User target') ->groups(['api', 'users']) ->label('audits.event', 'target.update') ->label('audits.resource', 'target/{response.$id}') @@ -1232,19 +1240,19 @@ App::patch('/v1/users/:userId/targets/:targetId/identifier') ->label('scope', 'targets.write') ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'users') - ->label('sdk.method', 'updateTargetIdentifier') - ->label('sdk.description', '/docs/references/users/update-target-identifier.md') + ->label('sdk.method', 'updateTarget') + ->label('sdk.description', '/docs/references/users/update-target.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_TARGET) ->param('userId', '', new UID(), 'User ID.') ->param('targetId', '', new UID(), 'Target ID.') - ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)') + ->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true) + ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)', true) ->inject('queueForEvents') ->inject('response') ->inject('dbForProject') - ->action(function (string $userId, string $targetId, string $identifier, Event $queueForEvents, Response $response, Database $dbForProject) { - + ->action(function (string $userId, string $targetId, string $providerId, string $identifier, Event $queueForEvents, Response $response, Database $dbForProject) { $user = $dbForProject->getDocument('users', $userId); if ($user->isEmpty()) { @@ -1261,7 +1269,20 @@ App::patch('/v1/users/:userId/targets/:targetId/identifier') throw new Exception(Exception::USER_TARGET_NOT_FOUND); } - $target->setAttribute('identifier', $identifier); + if ($identifier) { + $target->setAttribute('identifier', $identifier); + } + + if ($providerId) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + + $target->setAttribute('providerId', $provider->getId()); + $target->setAttribute('providerInternalId', $provider->getInternalId()); + } $target = $dbForProject->updateDocument('targets', $target->getId(), $target); $dbForProject->deleteCachedDocument('users', $user->getId()); diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 8b61580b76..1b0c993e11 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -857,10 +857,10 @@ App::post('/v1/vcs/github/events') $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); //find functionId from functions table - $repositories = $dbForConsole->find('repositories', [ + $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::limit(100), - ]); + ])); // create new deployment only on push and not when branch is created if (!$providerBranchCreated) { @@ -877,13 +877,13 @@ App::post('/v1/vcs/github/events') ]); foreach ($installations as $installation) { - $repositories = $dbForConsole->find('repositories', [ + $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ Query::equal('installationInternalId', [$installation->getInternalId()]), Query::limit(1000) - ]); + ])); foreach ($repositories as $repository) { - $dbForConsole->deleteDocument('repositories', $repository->getId()); + Authorization::skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId())); } $dbForConsole->deleteDocument('installations', $installation->getId()); @@ -915,10 +915,10 @@ App::post('/v1/vcs/github/events') $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; - $repositories = $dbForConsole->find('repositories', [ + $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') - ]); + ])); $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request); } elseif ($parsedPayload["action"] == "closed") { @@ -929,10 +929,10 @@ App::post('/v1/vcs/github/events') $external = $parsedPayload["external"] ?? true; if ($external) { - $repositories = $dbForConsole->find('repositories', [ + $repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') - ]); + ])); foreach ($repositories as $repository) { $providerPullRequestIds = $repository->getAttribute('providerPullRequestIds', []); @@ -1046,8 +1046,8 @@ App::delete('/v1/vcs/installations/:installationId') ->inject('response') ->inject('project') ->inject('dbForConsole') - ->inject('deletes') - ->action(function (string $installationId, Response $response, Document $project, Database $dbForConsole, Delete $deletes) { + ->inject('queueForDeletes') + ->action(function (string $installationId, Response $response, Document $project, Database $dbForConsole, Delete $queueForDeletes) { $installation = $dbForConsole->getDocument('installations', $installationId); if ($installation->isEmpty()) { @@ -1058,7 +1058,7 @@ App::delete('/v1/vcs/installations/:installationId') throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove installation from DB'); } - $deletes + $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($installation); @@ -1092,9 +1092,9 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor throw new Exception(Exception::INSTALLATION_NOT_FOUND); } - $repository = $dbForConsole->getDocument('repositories', $repositoryId, [ + $repository = Authorization::skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [ Query::equal('projectInternalId', [$project->getInternalId()]) - ]); + ])); if ($repository->isEmpty()) { throw new Exception(Exception::REPOSITORY_NOT_FOUND); @@ -1109,7 +1109,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor // TODO: Delete from array when PR is closed - $repository = $dbForConsole->updateDocument('repositories', $repository->getId(), $repository); + $repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository)); $privateKey = App::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); $githubAppId = App::getEnv('_APP_VCS_GITHUB_APP_ID'); diff --git a/app/init.php b/app/init.php index 094ca412fe..6f61b955f3 100644 --- a/app/init.php +++ b/app/init.php @@ -105,8 +105,8 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours const APP_USER_ACCCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours -const APP_CACHE_BUSTER = 516; -const APP_VERSION_STABLE = '1.4.11'; +const APP_CACHE_BUSTER = 327; +const APP_VERSION_STABLE = '1.4.12'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; @@ -557,6 +557,72 @@ Database::addFilter( return []; } ); + +Database::addFilter( + 'providerSearch', + function (mixed $value, Document $provider) { + $searchValues = [ + $provider->getId(), + $provider->getAttribute('name', ''), + $provider->getAttribute('provider', ''), + $provider->getAttribute('type', '') + ]; + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +Database::addFilter( + 'topicSearch', + function (mixed $value, Document $topic) { + $searchValues = [ + $topic->getId(), + $topic->getAttribute('name', ''), + $topic->getAttribute('description', ''), + ]; + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +Database::addFilter( + 'messageSearch', + function (mixed $value, Document $message) { + $searchValues = [ + $message->getId(), + $message->getAttribute('description', ''), + $message->getAttribute('status', ''), + ]; + + $data = \json_decode($message->getAttribute('data', []), true); + + if (\array_key_exists('subject', $data)) { + $searchValues = \array_merge($searchValues, [$data['subject'], 'email']); + } elseif (\array_key_exists('content', $data)) { + $searchValues = \array_merge($searchValues, [$data['content'], 'sms']); + } else { + $searchValues = \array_merge($searchValues, [$data['title'], 'push']); + } + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + /** * DB Formats */ diff --git a/bin/patch-recreate-repositories-documents b/bin/patch-recreate-repositories-documents new file mode 100644 index 0000000000..8c6c4157f4 --- /dev/null +++ b/bin/patch-recreate-repositories-documents @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/cli.php patch-recreate-repositories-documents $@ \ No newline at end of file diff --git a/composer.lock b/composer.lock index 25f0c3964d..afa71aaf2b 100644 --- a/composer.lock +++ b/composer.lock @@ -1906,16 +1906,16 @@ }, { "name": "utopia-php/database", - "version": "0.45.1", + "version": "0.45.2", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "0e76f996439b80794ab73c2fffdb51ebd6676e4b" + "reference": "dc789f2c1fd8b5ee07ff883e11c9ad7970824788" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/0e76f996439b80794ab73c2fffdb51ebd6676e4b", - "reference": "0e76f996439b80794ab73c2fffdb51ebd6676e4b", + "url": "https://api.github.com/repos/utopia-php/database/zipball/dc789f2c1fd8b5ee07ff883e11c9ad7970824788", + "reference": "dc789f2c1fd8b5ee07ff883e11c9ad7970824788", "shasum": "" }, "require": { @@ -1956,9 +1956,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.45.1" + "source": "https://github.com/utopia-php/database/tree/0.45.2" }, - "time": "2023-11-01T08:30:19+00:00" + "time": "2023-11-15T03:38:47+00:00" }, { "name": "utopia-php/domains", @@ -5822,5 +5822,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/docker-compose.yml b/docker-compose.yml index 9505f3e930..04d9f60d23 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -587,6 +587,8 @@ services: - _APP_DB_PASS - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG + - _APP_SMS_FROM + - _APP_SMS_PROVIDER appwrite-worker-migrations: entrypoint: worker-migrations diff --git a/src/Appwrite/Event/Messaging.php b/src/Appwrite/Event/Messaging.php index 76abecd1c0..62d41f8c3b 100644 --- a/src/Appwrite/Event/Messaging.php +++ b/src/Appwrite/Event/Messaging.php @@ -9,7 +9,11 @@ use Utopia\Queue\Client; class Messaging extends Event { protected ?string $messageId = null; - private ?string $deliveryTime = null; + protected ?Document $message = null; + protected ?array $recipients = null; + protected ?string $deliveryTime = null; + protected ?string $providerType = null; + public function __construct(protected Connection $connection) { @@ -20,6 +24,52 @@ class Messaging extends Event ->setClass(Event::MESSAGING_CLASS_NAME); } + /** + * Sets recipient for the messaging event. + * + * @param string[] $recipients + * @return self + */ + public function setRecipients(array $recipients): self + { + $this->recipients = $recipients; + + return $this; + } + + /** + * Returns set recipient for messaging event. + * + * @return string[] + */ + public function getRecipient(): array + { + return $this->recipients; + } + + /** + * Sets message document for the messaging event. + * + * @param Document $message + * @return self + */ + public function setMessage(Document $message): self + { + $this->message = $message; + + return $this; + } + + /** + * Returns message document for the messaging event. + * + * @return string + */ + public function getMessage(): Document + { + return $this->message; + } + /** * Sets message ID for the messaging event. * @@ -43,6 +93,29 @@ class Messaging extends Event return $this->messageId; } + /** + * Sets provider type for the messaging event. + * + * @param string $providerType + * @return self + */ + public function setProviderType(string $providerType): self + { + $this->providerType = $providerType; + + return $this; + } + + /** + * Returns set provider type for the messaging event. + * + * @return string + */ + public function getProviderType(): string + { + return $this->providerType; + } + /** * Sets Delivery time for the messaging event. * @@ -92,6 +165,9 @@ class Messaging extends Event 'project' => $this->project, 'user' => $this->user, 'messageId' => $this->messageId, + 'message' => $this->message, + 'recipients' => $this->recipients, + 'providerType' => $this->providerType, ]); } } diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index d887a5a520..3869444752 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -84,8 +84,11 @@ class Exception extends \Exception public const USER_OAUTH2_BAD_REQUEST = 'user_oauth2_bad_request'; public const USER_OAUTH2_UNAUTHORIZED = 'user_oauth2_unauthorized'; public const USER_OAUTH2_PROVIDER_ERROR = 'user_oauth2_provider_error'; + public const USER_EMAIL_ALREADY_VERIFIED = 'user_email_alread_verified'; + public const USER_PHONE_ALREADY_VERIFIED = 'user_phone_already_verified'; public const USER_TARGET_NOT_FOUND = 'user_target_not_found'; public const USER_TARGET_ALREADY_EXISTS = 'user_target_already_exists'; + /** Teams */ public const TEAM_NOT_FOUND = 'team_not_found'; public const TEAM_INVITE_ALREADY_EXISTS = 'team_invite_already_exists'; @@ -236,6 +239,7 @@ class Exception extends \Exception public const PROVIDER_NOT_FOUND = 'provider_not_found'; public const PROVIDER_ALREADY_EXISTS = 'provider_already_exists'; public const PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type'; + public const PROVIDER_INTERNAL_UPDATE_DISABLED = 'provider_internal_update_disabled'; /** Topic */ public const TOPIC_NOT_FOUND = 'topic_not_found'; diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index 406acae7df..8f68e31be4 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -76,6 +76,7 @@ abstract class Migration '1.4.9' => 'V19', '1.4.10' => 'V19', '1.4.11' => 'V19', + '1.4.12' => 'V19' ]; /** diff --git a/src/Appwrite/Platform/Services/Tasks.php b/src/Appwrite/Platform/Services/Tasks.php index e725ff5f3e..28d7046dd1 100644 --- a/src/Appwrite/Platform/Services/Tasks.php +++ b/src/Appwrite/Platform/Services/Tasks.php @@ -19,6 +19,7 @@ use Appwrite\Platform\Tasks\VolumeSync; use Appwrite\Platform\Tasks\CalcTierStats; use Appwrite\Platform\Tasks\Upgrade; use Appwrite\Platform\Tasks\DeleteOrphanedProjects; +use Appwrite\Platform\Tasks\PatchRecreateRepositoriesDocuments; class Tasks extends Service { @@ -42,6 +43,7 @@ class Tasks extends Service ->addAction(Specs::getName(), new Specs()) ->addAction(CalcTierStats::getName(), new CalcTierStats()) ->addAction(DeleteOrphanedProjects::getName(), new DeleteOrphanedProjects()) + ->addAction(PatchRecreateRepositoriesDocuments::getName(), new PatchRecreateRepositoriesDocuments()) ; } diff --git a/src/Appwrite/Platform/Tasks/DeleteOrphanedProjects.php b/src/Appwrite/Platform/Tasks/DeleteOrphanedProjects.php index 2824f4e286..753240b66a 100644 --- a/src/Appwrite/Platform/Tasks/DeleteOrphanedProjects.php +++ b/src/Appwrite/Platform/Tasks/DeleteOrphanedProjects.php @@ -2,17 +2,16 @@ namespace Appwrite\Platform\Tasks; -use PHPMailer\PHPMailer\PHPMailer; use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; use Utopia\Platform\Action; use Utopia\Cache\Cache; use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Pools\Group; use Utopia\Registry\Registry; +use Utopia\Validator\Boolean; class DeleteOrphanedProjects extends Action { @@ -25,18 +24,19 @@ class DeleteOrphanedProjects extends Action { $this - ->desc('Get stats for projects') + ->desc('Delete orphaned projects') + ->param('commit', false, new Boolean(true), 'Commit project deletion', true) ->inject('pools') ->inject('cache') ->inject('dbForConsole') ->inject('register') - ->callback(function (Group $pools, Cache $cache, Database $dbForConsole, Registry $register) { - $this->action($pools, $cache, $dbForConsole, $register); + ->callback(function (bool $commit, Group $pools, Cache $cache, Database $dbForConsole, Registry $register) { + $this->action($commit, $pools, $cache, $dbForConsole, $register); }); } - public function action(Group $pools, Cache $cache, Database $dbForConsole, Registry $register): void + public function action(bool $commit, Group $pools, Cache $cache, Database $dbForConsole, Registry $register): void { Console::title('Delete orphaned projects V1'); @@ -55,6 +55,7 @@ class DeleteOrphanedProjects extends Action Console::success("Found a total of: {$totalProjects} projects"); $orphans = 0; + $cnt = 0; $count = 0; $limit = 30; $sum = 30; @@ -79,19 +80,43 @@ class DeleteOrphanedProjects extends Action $dbForProject = new Database($adapter, $cache); $dbForProject->setDefaultDatabase('appwrite'); $dbForProject->setNamespace('_' . $project->getInternalId()); - $collectionsCreated = $dbForProject->count(Database::METADATA); - $message = ' (' . $collectionsCreated . ') collections where found on project (' . $project->getId() . '))'; - if ($collectionsCreated < (count($collectionsConfig) + 2)) { - Console::error($message); - $orphans++; - } else { - Console::log($message); + $collectionsCreated = 0; + $cnt++; + if ($dbForProject->exists($dbForProject->getDefaultDatabase(), Database::METADATA)) { + $collectionsCreated = $dbForProject->count(Database::METADATA); } - } catch (\Throwable $th) { - //$dbForConsole->deleteDocument('projects', $project->getId()); - //Console::success('Deleting project (' . $project->getId() . ')'); - Console::error(' (0) collections where found for project (' . $project->getId() . ')'); + + $msg = '(' . $cnt . ') found (' . $collectionsCreated . ') collections on project (' . $project->getInternalId() . ') , database (' . $project['database'] . ')'; + /** + * +2 = audit+abuse + */ + if ($collectionsCreated >= (count($collectionsConfig) + 2)) { + Console::log($msg . ' ignoring....'); + continue; + } + + Console::log($msg); + + if ($collectionsCreated > 0) { + $collections = $dbForProject->find(Database::METADATA, []); + foreach ($collections as $collection) { + if ($commit) { + $dbForProject->deleteCollection($collection->getId()); + $dbForConsole->deleteCachedCollection($collection->getId()); + } + Console::info('--Deleting collection (' . $collection->getId() . ') project no (' . $project->getInternalId() . ')'); + } + } + if ($commit) { + $dbForConsole->deleteDocument('projects', $project->getId()); + $dbForConsole->deleteCachedDocument('projects', $project->getId()); + } + + Console::info('--Deleting project no (' . $project->getInternalId() . ')'); + $orphans++; + } catch (\Throwable $th) { + Console::error('Error: ' . $th->getMessage()); } finally { $pools ->get($db) @@ -110,6 +135,6 @@ class DeleteOrphanedProjects extends Action $count = $count + $sum; } - Console::log('Iterated through ' . $count - 1 . '/' . $totalProjects . ' projects found ' . $orphans . ' orphans'); + Console::log('Iterated through ' . $count - 1 . '/' . $totalProjects . ' projects found ' . $orphans . ' orphans'); } } diff --git a/src/Appwrite/Platform/Tasks/PatchRecreateRepositoriesDocuments.php b/src/Appwrite/Platform/Tasks/PatchRecreateRepositoriesDocuments.php new file mode 100644 index 0000000000..93e6c527bb --- /dev/null +++ b/src/Appwrite/Platform/Tasks/PatchRecreateRepositoriesDocuments.php @@ -0,0 +1,169 @@ +desc('Recreate missing repositories in consoleDB from projectDBs. They can be missing if you used Appwrite 1.4.10 or 1.4.11, and deleted a function.') + ->param('after', '', new Text(36), 'After cursor', true) + ->param('projectId', '', new Text(36), 'Select project to validate', true) + ->inject('dbForConsole') + ->inject('getProjectDB') + ->callback(fn ($after, $projectId, $dbForConsole, $getProjectDB) => $this->action($after, $projectId, $dbForConsole, $getProjectDB)); + } + + public function action($after, $projectId, Database $dbForConsole, callable $getProjectDB): void + { + Console::info("Starting the patch"); + + $startTime = microtime(true); + + if (!empty($projectId)) { + try { + $project = $dbForConsole->getDocument('projects', $projectId); + $dbForProject = call_user_func($getProjectDB, $project); + $this->recreateRepositories($dbForConsole, $dbForProject, $project); + } catch (\Throwable $th) { + Console::error("Unexpected error occured with Project ID {$projectId}"); + Console::error('[Error] Type: ' . get_class($th)); + Console::error('[Error] Message: ' . $th->getMessage()); + Console::error('[Error] File: ' . $th->getFile()); + Console::error('[Error] Line: ' . $th->getLine()); + } + } else { + $queries = []; + if (!empty($after)) { + Console::info("Iterating remaining projects after project with ID {$after}"); + $project = $dbForConsole->getDocument('projects', $after); + $queries = [Query::cursorAfter($project)]; + } else { + Console::info("Iterating all projects"); + } + $this->foreachDocument($dbForConsole, 'projects', $queries, function (Document $project) use ($getProjectDB, $dbForConsole) { + $projectId = $project->getId(); + + try { + $dbForProject = call_user_func($getProjectDB, $project); + $this->recreateRepositories($dbForConsole, $dbForProject, $project); + } catch (\Throwable $th) { + Console::error("Unexpected error occured with Project ID {$projectId}"); + Console::error('[Error] Type: ' . get_class($th)); + Console::error('[Error] Message: ' . $th->getMessage()); + Console::error('[Error] File: ' . $th->getFile()); + Console::error('[Error] Line: ' . $th->getLine()); + } + }); + } + + $endTime = microtime(true); + $timeTaken = $endTime - $startTime; + + $hours = (int)($timeTaken / 3600); + $timeTaken -= $hours * 3600; + $minutes = (int)($timeTaken / 60); + $timeTaken -= $minutes * 60; + $seconds = (int)$timeTaken; + $milliseconds = ($timeTaken - $seconds) * 1000; + Console::info("Recreate patch completed in $hours h, $minutes m, $seconds s, $milliseconds mis ( total $timeTaken milliseconds)"); + } + + protected function foreachDocument(Database $database, string $collection, array $queries = [], callable $callback = null): void + { + $limit = 1000; + $results = []; + $sum = $limit; + $latestDocument = null; + + while ($sum === $limit) { + $newQueries = $queries; + + if ($latestDocument != null) { + array_unshift($newQueries, Query::cursorAfter($latestDocument)); + } + $newQueries[] = Query::limit($limit); + $results = $database->find($collection, $newQueries); + + if (empty($results)) { + return; + } + + $sum = count($results); + + foreach ($results as $document) { + if (is_callable($callback)) { + $callback($document); + } + } + $latestDocument = $results[array_key_last($results)]; + } + } + + public function recreateRepositories(Database $dbForConsole, Database $dbForProject, Document $project): void + { + $projectId = $project->getId(); + Console::log("Running patch for project {$projectId}"); + + $this->foreachDocument($dbForProject, 'functions', [], function (Document $function) use ($dbForProject, $dbForConsole, $project) { + $isConnected = !empty($function->getAttribute('providerRepositoryId', '')); + + if ($isConnected) { + $repository = $dbForConsole->getDocument('repositories', $function->getAttribute('repositoryId', '')); + + if ($repository->isEmpty()) { + $projectId = $project->getId(); + $functionId = $function->getId(); + Console::success("Recreating repositories document for project ID {$projectId}, function ID {$functionId}"); + + $repository = $dbForConsole->createDocument('repositories', new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'installationId' => $function->getAttribute('installationId', ''), + 'installationInternalId' => $function->getAttribute('installationInternalId', ''), + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'providerRepositoryId' => $function->getAttribute('providerRepositoryId', ''), + 'resourceId' => $function->getId(), + 'resourceInternalId' => $function->getInternalId(), + 'resourceType' => 'function', + 'providerPullRequestIds' => [] + ])); + + $function = $dbForProject->updateDocument('functions', $function->getId(), $function + ->setAttribute('repositoryId', $repository->getId()) + ->setAttribute('repositoryInternalId', $repository->getInternalId())); + + $this->foreachDocument($dbForProject, 'deployments', [ + Query::equal('resourceInternalId', [$function->getInternalId()]), + Query::equal('resourceType', ['functions']) + ], function (Document $deployment) use ($dbForProject, $repository) { + $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment + ->setAttribute('repositoryId', $repository->getId()) + ->setAttribute('repositoryInternalId', $repository->getInternalId())); + }); + } + } + }); + } +} diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 130daf862d..74365bad86 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -753,14 +753,15 @@ class Deletes extends Action */ Console::info("Deleting VCS repositories and comments linked to function " . $functionId); $this->deleteByGroup('repositories', [ + Query::equal('projectInternalId', [$project->getInternalId()]), Query::equal('resourceInternalId', [$functionInternalId]), Query::equal('resourceType', ['function']), ], $dbForConsole, function (Document $document) use ($dbForConsole) { $providerRepositoryId = $document->getAttribute('providerRepositoryId', ''); - $projectId = $document->getAttribute('projectId', ''); + $projectInternalId = $document->getAttribute('projectInternalId', ''); $this->deleteByGroup('vcsComments', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), - Query::equal('projectId', [$projectId]), + Query::equal('projectInternalId', [$projectInternalId]), ], $dbForConsole); }); diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 63e0ff651a..299ef4526f 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -3,14 +3,16 @@ namespace Appwrite\Platform\Workers; use Appwrite\Extend\Exception; +use Utopia\App; use Utopia\CLI\Console; +use Utopia\Database\Helpers\ID; +use Utopia\DSN\DSN; use Utopia\Platform\Action; use Utopia\Queue\Message; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; use Utopia\Messaging\Adapters\SMS as SMSAdapter; use Utopia\Messaging\Adapters\SMS\Mock; use Utopia\Messaging\Adapters\SMS\Msg91; @@ -64,9 +66,15 @@ class Messaging extends Action return; } - $message = $dbForProject->getDocument('messages', $payload['messageId']); + if (!\is_null($payload['message']) && !\is_null($payload['recipients'])) { + if ($payload['providerType'] === 'SMS') { + $this->processInternalSMSMessage(new Document($payload['message']), $payload['recipients']); + } + } else { + $message = $dbForProject->getDocument('messages', $payload['messageId']); - $this->processMessage($dbForProject, $message); + $this->processMessage($dbForProject, $message); + } } private function processMessage(Database $dbForProject, Document $message): void @@ -99,28 +107,56 @@ class Messaging extends Action $recipients = \array_merge($recipients, $targets); } + $internalProvider = $dbForProject->findOne('providers', [ + Query::equal('enabled', [true]), + Query::equal('type', [$recipients[0]->getAttribute('providerType')]), + ]); + + /** + * @var array> $identifiersByProviderId + */ + $identifiersByProviderId = []; + + /** + * @var Document[] $providers + */ $providers = []; foreach ($recipients as $recipient) { $providerId = $recipient->getAttribute('providerId'); - if (!isset($providers[$providerId])) { - $providers[$providerId] = []; + + if (!$providerId) { + $providerId = $internalProvider->getId(); } - $providers[$providerId][] = $recipient->getAttribute('identifier'); + + if (!isset($identifiersByProviderId[$providerId])) { + $identifiersByProviderId[$providerId] = []; + } + $identifiersByProviderId[$providerId][] = $recipient->getAttribute('identifier'); } /** * @var array[] $results */ - $results = batch(\array_map(function ($providerId) use ($providers, $message, $dbForProject) { - return function () use ($providerId, $providers, $message, $dbForProject) { - $provider = $dbForProject->getDocument('providers', $providerId); - $identifiers = $providers[$providerId]; + $results = batch(\array_map(function ($providerId) use ($identifiersByProviderId, $providers, $internalProvider, $message, $dbForProject) { + return function () use ($providerId, $identifiersByProviderId, $providers, $internalProvider, $message, $dbForProject) { + $provider = new Document(); + + if ($internalProvider->getId() === $providerId) { + $provider = $internalProvider; + } else { + $provider = $dbForProject->getDocument('providers', $providerId); + } + + $providers[] = $provider; + $identifiers = $identifiersByProviderId[$providerId]; + $adapter = match ($provider->getAttribute('type')) { 'sms' => $this->sms($provider), 'push' => $this->push($provider), 'email' => $this->email($provider), default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) }; + $maxBatchSize = $adapter->getMaxMessagesPerRequest(); $batches = \array_chunk($identifiers, $maxBatchSize); $batchIndex = 0; @@ -131,12 +167,14 @@ class Messaging extends Action $deliveryErrors = []; $messageData = clone $message; $messageData->setAttribute('to', $batch); + $data = match ($provider->getAttribute('type')) { 'sms' => $this->buildSMSMessage($messageData, $provider), 'push' => $this->buildPushMessage($messageData), 'email' => $this->buildEmailMessage($messageData, $provider), default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) }; + try { $adapter->send($data); $deliveredTotal += \count($batch); @@ -154,16 +192,18 @@ class Messaging extends Action return $results; }; - }, \array_keys($providers))); + }, \array_keys($identifiersByProviderId))); $results = array_merge(...$results); $deliveredTotal = 0; $deliveryErrors = []; + foreach ($results as $result) { $deliveredTotal += $result['deliveredTotal']; $deliveryErrors = \array_merge($deliveryErrors, $result['deliveryErrors']); } + $message->setAttribute('deliveryErrors', $deliveryErrors); if (\count($message->getAttribute('deliveryErrors')) > 0) { @@ -171,13 +211,88 @@ class Messaging extends Action } else { $message->setAttribute('status', 'sent'); } + $message->removeAttribute('to'); + + foreach ($providers as $provider) { + $message->setAttribute('search', "{$message->getAttribute('search')} {$provider->getAttribute('name')} {$provider->getAttribute('provider')} {$provider->getAttribute('type')}"); + } + $message->setAttribute('deliveredTotal', $deliveredTotal); $message->setAttribute('deliveredAt', DateTime::now()); $dbForProject->updateDocument('messages', $message->getId(), $message); } + private function processInternalSMSMessage(Document $message, array $recipients): void + { + if (empty(App::getEnv('_APP_SMS_PROVIDER')) || empty(App::getEnv('_APP_SMS_FROM'))) { + Console::info('Skipped SMS processing. No Phone configuration has been set.'); + return; + } + + $smsDSN = new DSN(App::getEnv('_APP_SMS_PROVIDER')); + $host = $smsDSN->getHost(); + $password = $smsDSN->getPassword(); + $user = $smsDSN->getUser(); + + $from = App::getEnv('_APP_SMS_FROM'); + + $provider = new Document([ + '$id' => ID::unique(), + 'provider' => $host, + 'type' => 'sms', + 'name' => 'Internal SMS', + 'enabled' => true, + 'credentials' => match ($host) { + 'twilio' => [ + 'accountSid' => $user, + 'authToken' => $password + ], + 'textmagic' => [ + 'username' => $user, + 'apiKey' => $password + ], + 'telesign' => [ + 'username' => $user, + 'password' => $password + ], + 'msg91' => [ + 'senderId' => $user, + 'authKey' => $password + ], + 'vonage' => [ + 'apiKey' => $user, + 'apiSecret' => $password + ], + default => null + }, + 'options' => [ + 'from' => $from + ] + ]); + + $adapter = $this->sms($provider); + + $maxBatchSize = $adapter->getMaxMessagesPerRequest(); + $batches = \array_chunk($recipients, $maxBatchSize); + $batchIndex = 0; + + batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex) { + return function () use ($batch, $message, $provider, $adapter, $batchIndex) { + $message->setAttribute('to', $batch); + + $data = $this->buildSMSMessage($message, $provider); + + try { + $adapter->send($data); + } catch (\Exception $e) { + Console::error('Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage()); + } + }; + }, $batches)); + } + public function shutdown(): void { } @@ -188,7 +303,7 @@ class Messaging extends Action return match ($provider->getAttribute('provider')) { 'mock' => new Mock('username', 'password'), 'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken']), - 'text-magic' => new TextMagic($credentials['username'], $credentials['apiKey']), + 'textmagic' => new TextMagic($credentials['username'], $credentials['apiKey']), 'telesign' => new Telesign($credentials['username'], $credentials['password']), 'msg91' => new Msg91($credentials['senderId'], $credentials['authKey']), 'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']), diff --git a/src/Appwrite/Usage/Calculators/TimeSeries.php b/src/Appwrite/Usage/Calculators/TimeSeries.php index e0a12b443f..6dbf49c44c 100644 --- a/src/Appwrite/Usage/Calculators/TimeSeries.php +++ b/src/Appwrite/Usage/Calculators/TimeSeries.php @@ -8,6 +8,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use InfluxDB\Database as InfluxDatabase; use DateTime; +use Utopia\Database\Validator\Authorization; use Utopia\Registry\Registry; class TimeSeries extends Calculator @@ -426,32 +427,34 @@ class TimeSeries extends Calculator $project = $this->database->getDocument('projects', $projectId); $database = call_user_func($this->getProjectDB, $project); - try { - $document = $database->getDocument('stats', $id); - if ($document->isEmpty()) { - $database->createDocument('stats', new Document([ - '$id' => $id, - 'period' => $period, - 'time' => $time, - 'metric' => $metric, - 'value' => $value, - 'type' => $type, - 'region' => $this->region, - ])); - } else { - $database->updateDocument( - 'stats', - $document->getId(), - $document->setAttribute('value', $value) - ); + Authorization::skip(function () use ($database, $id, $period, $time, $metric, $value, $type, $projectId) { + try { + $document = $database->getDocument('stats', $id); + if ($document->isEmpty()) { + $database->createDocument('stats', new Document([ + '$id' => $id, + 'period' => $period, + 'time' => $time, + 'metric' => $metric, + 'value' => $value, + 'type' => $type, + 'region' => $this->region, + ])); + } else { + $database->updateDocument( + 'stats', + $document->getId(), + $document->setAttribute('value', $value) + ); + } + } catch (\Exception $e) { // if projects are deleted this might fail + if (is_callable($this->errorHandler)) { + call_user_func($this->errorHandler, $e, "sync_project_{$projectId}_metric_{$metric}"); + } else { + throw $e; + } } - } catch (\Exception $e) { // if projects are deleted this might fail - if (is_callable($this->errorHandler)) { - call_user_func($this->errorHandler, $e, "sync_project_{$projectId}_metric_{$metric}"); - } else { - throw $e; - } - } + }); $this->register->get('pools')->reclaim(); } diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Providers.php b/src/Appwrite/Utopia/Database/Validator/Queries/Providers.php index 7760acbdad..a28f5e8437 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Providers.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Providers.php @@ -8,7 +8,6 @@ class Providers extends Base 'name', 'provider', 'type', - 'internal', 'enabled', ]; diff --git a/src/Appwrite/Utopia/Response/Model/Provider.php b/src/Appwrite/Utopia/Response/Model/Provider.php index a121753275..f8a0514020 100644 --- a/src/Appwrite/Utopia/Response/Model/Provider.php +++ b/src/Appwrite/Utopia/Response/Model/Provider.php @@ -40,12 +40,6 @@ class Provider extends Model 'default' => '', 'example' => 'mailgun', ]) - ->addRule('internal', [ - 'type' => self::TYPE_BOOLEAN, - 'description' => 'Is this a pre-configured provider instance?', - 'default' => false, - 'example' => true, - ]) ->addRule('enabled', [ 'type' => self::TYPE_BOOLEAN, 'description' => 'Is provider enabled?', diff --git a/src/Appwrite/Utopia/Response/Model/Subscriber.php b/src/Appwrite/Utopia/Response/Model/Subscriber.php index 65bbd38f0e..5080b44333 100644 --- a/src/Appwrite/Utopia/Response/Model/Subscriber.php +++ b/src/Appwrite/Utopia/Response/Model/Subscriber.php @@ -34,6 +34,18 @@ class Subscriber extends Model 'default' => '', 'example' => '259125845563242502', ]) + ->addRule('userId', [ + 'type' => self::TYPE_STRING, + 'description' => 'User ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('userName', [ + 'type' => self::TYPE_STRING, + 'description' => 'User Name.', + 'default' => '', + 'example' => 'Aegon Targaryen', + ]) ->addRule('topicId', [ 'type' => self::TYPE_STRING, 'description' => 'Topic ID.', diff --git a/src/Appwrite/Utopia/Response/Model/Target.php b/src/Appwrite/Utopia/Response/Model/Target.php index c6c5929ee3..f6346f409a 100644 --- a/src/Appwrite/Utopia/Response/Model/Target.php +++ b/src/Appwrite/Utopia/Response/Model/Target.php @@ -41,6 +41,12 @@ class Target extends Model 'default' => '', 'example' => '259125845563242502', ]) + ->addRule('providerType', [ + 'type' => self::TYPE_STRING, + 'description' => 'The target provider type. Can be one of the following: `email`, `sms` or `push`.', + 'default' => '', + 'example' => 'email', + ]) ->addRule('identifier', [ 'type' => self::TYPE_STRING, 'description' => 'The target identifier.', diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index e64cf2c4ca..953b89ced7 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -945,6 +945,32 @@ trait AccountBase return $data; } + /** + * @depends testUpdateAccountVerification + */ + public function testCreateAccountVerificationForVerifiedEmail($data): array + { + $email = $data['email'] ?? ''; + $name = $data['name'] ?? ''; + $session = $data['session'] ?? ''; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_POST, '/account/verification', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ]), [ + 'url' => 'http://localhost/verification', + ]); + + $this->assertEquals(409, $response['headers']['status-code']); + + return $data; + } + /** * @depends testUpdateAccountVerification */ diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index d56f93a42c..6d75f75512 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -744,33 +744,8 @@ class AccountCustomClientTest extends Scope public function testCreatePhone(): array { - if (empty(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN'))) { - $this->markTestSkipped('SMS DSN not provided'); - } + $number = '+123456789'; - $smsDSN = new DSN(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN')); - $to = $smsDSN->getParam('to'); - $from = $smsDSN->getParam('from'); - $authKey = $smsDSN->getPassword(); - $senderId = $smsDSN->getUser(); - - if (empty($to) || empty($from) || empty($authKey) || empty($senderId)) { - $this->markTestSkipped('SMS provider not configured'); - } - - $number = $to; - $response = $this->client->call(Client::METHOD_POST, '/messaging/providers/msg91', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), [ - 'providerId' => ID::unique(), - 'name' => 'Sms provider', - 'senderId' => $senderId, - 'authKey' => $authKey, - 'from' => $from, - ]); - $this->assertEquals(201, $response['headers']['status-code']); /** * Test for SUCCESS */ @@ -781,7 +756,6 @@ class AccountCustomClientTest extends Scope ]), [ 'userId' => ID::unique(), 'phone' => $number, - 'from' => $from, ]); $this->assertEquals(201, $response['headers']['status-code']); @@ -807,19 +781,17 @@ class AccountCustomClientTest extends Scope \sleep(5); - $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $messageId, [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); + $smsRequest = $this->getLastRequest(); - $this->assertEquals(200, $message['headers']['status-code']); - $this->assertEquals(1, $message['body']['deliveredTotal']); - $this->assertEquals(0, \count($message['body']['deliveryErrors'])); + $this->assertEquals('http://request-catcher:5000/mock-sms', $smsRequest['url']); + $this->assertEquals('Appwrite Mock Message Sender', $smsRequest['headers']['User-Agent']); + $this->assertEquals('username', $smsRequest['headers']['X-Username']); + $this->assertEquals('password', $smsRequest['headers']['X-Key']); + $this->assertEquals('POST', $smsRequest['method']); + $this->assertEquals('+123456789', $smsRequest['data']['from']); + $this->assertEquals($number, $smsRequest['data']['to']); - - $data['token'] = $message['body']['data']['content']; + $data['token'] = $smsRequest['data']['message']; $data['id'] = $userId; $data['number'] = $number; @@ -1018,8 +990,6 @@ class AccountCustomClientTest extends Scope public function testPhoneVerification(array $data): array { $session = $data['session'] ?? ''; - $smsDSN = new DSN(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN')); - $from = $smsDSN->getParam('from'); /** * Test for SUCCESS @@ -1030,28 +1000,19 @@ class AccountCustomClientTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, - ]), ['from' => $from]); + ])); $this->assertEquals(201, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertEmpty($response['body']['secret']); $this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['expire'])); - \sleep(3); + \sleep(2); - $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $response['body']['$id'], [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); - - $this->assertEquals(200, $message['headers']['status-code']); - $this->assertEquals(1, $message['body']['deliveredTotal']); - $this->assertEquals(0, \count($message['body']['deliveryErrors'])); + $smsRequest = $this->getLastRequest(); return \array_merge($data, [ - 'token' => $message['body']['data']['content'] + 'token' => $smsRequest['data']['secret'] ]); } @@ -1108,4 +1069,27 @@ class AccountCustomClientTest extends Scope return $data; } + + /** + * @depends testPhoneVerification + */ + #[Retry(count: 1)] + public function testPhoneVerificationForVerifiedPhone(array $data): array + { + $session = $data['session'] ?? ''; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_POST, '/account/verification/phone', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ])); + + $this->assertEquals(409, $response['headers']['status-code']); + + return $data; + } } diff --git a/tests/e2e/Services/GraphQL/AccountTest.php b/tests/e2e/Services/GraphQL/AccountTest.php index 9d1da09feb..3e2cfac29c 100644 --- a/tests/e2e/Services/GraphQL/AccountTest.php +++ b/tests/e2e/Services/GraphQL/AccountTest.php @@ -124,38 +124,7 @@ class AccountTest extends Scope */ public function testCreatePhoneVerification(): array { - if (empty(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN'))) { - $this->markTestSkipped('SMS DSN not provided'); - } - - $smsDSN = new DSN(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN')); - $to = $smsDSN->getParam('to'); - $from = $smsDSN->getParam('from'); - $authKey = $smsDSN->getPassword(); - $senderId = $smsDSN->getUser(); - - if (empty($to) || empty($from) || empty($authKey) || empty($senderId)) { - $this->markTestSkipped('SMS provider not configured'); - } - $projectId = $this->getProject()['$id']; - $query = $this->getQuery(self::$CREATE_MSG91_PROVIDER); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'providerId' => ID::unique(), - 'name' => 'Sms Provider', - 'from' => $from, - 'senderId' => $senderId, - 'authKey' => $authKey, - ], - ]; - - $response = $this->client->call(Client::METHOD_POST, '/graphql', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $projectId, - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], $graphQLPayload); $query = $this->getQuery(self::$CREATE_PHONE_VERIFICATION); $graphQLPayload = [ diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 93fd6cdc74..8655395dc2 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -934,10 +934,11 @@ trait Base } }'; case self::$CREATE_USER_TARGET: - return 'mutation createUserTarget($userId: String!, $targetId: String!, $providerId: String!, $identifier: String!){ - usersCreateTarget(userId: $userId, targetId: $targetId, providerId: $providerId, identifier: $identifier) { + return 'mutation createUserTarget($userId: String!, $targetId: String!, $providerType: String!, $identifier: String! $providerId: String){ + usersCreateTarget(userId: $userId, targetId: $targetId, providerType: $providerType, identifier: $identifier, providerId: $providerId) { _id userId + providerType providerId identifier } @@ -949,6 +950,7 @@ trait Base targets { _id userId + providerType providerId identifier } @@ -959,15 +961,17 @@ trait Base usersGetTarget(userId: $userId, targetId: $targetId) { _id userId + providerType providerId identifier } }'; case self::$UPDATE_USER_TARGET: - return 'mutation updateUserTarget($userId: String!, $targetId: String!, $identifier: String!){ - usersUpdateTargetIdentifier(userId: $userId, targetId: $targetId, identifier: $identifier) { + return 'mutation updateUserTarget($userId: String!, $targetId: String!, $providerId: String, $identifier: String){ + usersUpdateTarget(userId: $userId, targetId: $targetId, providerId: $providerId, identifier: $identifier) { _id userId + providerType providerId identifier } @@ -1792,7 +1796,6 @@ trait Base name provider type - internal enabled } }'; @@ -1803,7 +1806,6 @@ trait Base name provider type - internal enabled } }'; @@ -1814,7 +1816,6 @@ trait Base name provider type - internal enabled } }'; @@ -1825,7 +1826,6 @@ trait Base name provider type - internal enabled } }'; @@ -1836,7 +1836,6 @@ trait Base name provider type - internal enabled } }'; @@ -1847,7 +1846,6 @@ trait Base name provider type - internal enabled } }'; @@ -1858,7 +1856,6 @@ trait Base name provider type - internal enabled } }'; @@ -1869,7 +1866,6 @@ trait Base name provider type - internal enabled } }'; @@ -1880,7 +1876,6 @@ trait Base name provider type - internal enabled } }'; @@ -1893,7 +1888,7 @@ trait Base name provider type - internal + enabled } } @@ -1905,7 +1900,6 @@ trait Base name provider type - internal enabled } }'; @@ -1916,7 +1910,6 @@ trait Base name provider type - internal enabled } }'; @@ -1927,7 +1920,6 @@ trait Base name provider type - internal enabled } }'; @@ -1938,7 +1930,6 @@ trait Base name provider type - internal enabled } }'; @@ -1949,7 +1940,6 @@ trait Base name provider type - internal enabled } }'; @@ -1960,7 +1950,6 @@ trait Base name provider type - internal enabled } }'; @@ -1971,7 +1960,6 @@ trait Base name provider type - internal enabled } }'; @@ -1982,7 +1970,6 @@ trait Base name provider type - internal enabled } }'; @@ -1993,7 +1980,6 @@ trait Base name provider type - internal enabled } }'; @@ -2004,7 +1990,6 @@ trait Base name provider type - internal enabled } }'; @@ -2059,6 +2044,7 @@ trait Base return 'mutation createSubscriber($subscriberId: String!, $targetId: String!, $topicId: String!) { messagingCreateSubscriber(subscriberId: $subscriberId, targetId: $targetId, topicId: $topicId) { _id + userId targetId topicId } @@ -2069,6 +2055,7 @@ trait Base total subscribers { _id + userId targetId topicId } @@ -2078,6 +2065,7 @@ trait Base return 'query getSubscriber($topicId: String!, $subscriberId: String!) { messagingGetSubscriber(topicId: $topicId, subscriberId: $subscriberId) { _id + userId targetId topicId } diff --git a/tests/e2e/Services/GraphQL/MessagingTest.php b/tests/e2e/Services/GraphQL/MessagingTest.php index c0322372ba..27b2b52cd3 100644 --- a/tests/e2e/Services/GraphQL/MessagingTest.php +++ b/tests/e2e/Services/GraphQL/MessagingTest.php @@ -395,6 +395,7 @@ class MessagingTest extends Scope 'query' => $query, 'variables' => [ 'targetId' => ID::unique(), + 'providerType' => 'email', 'userId' => $userId, 'providerId' => $providerId, 'identifier' => 'token', @@ -427,20 +428,23 @@ class MessagingTest extends Scope ], $this->getHeaders()), $graphQLPayload); $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['topicId'], $topicId); + $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['targetId'], $targetId); + $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['userId'], $userId); return $response['body']['data']['messagingCreateSubscriber']; } /** - * @depends testUpdateTopic + * @depends testCreateSubscriber */ - public function testListSubscribers(string $topicId) + public function testListSubscribers(array $subscriber) { $query = $this->getQuery(self::$LIST_SUBSCRIBERS); $graphQLPayload = [ 'query' => $query, 'variables' => [ - 'topicId' => $topicId, + 'topicId' => $subscriber['topicId'], ], ]; $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ @@ -450,6 +454,9 @@ class MessagingTest extends Scope ]), $graphQLPayload); $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($response['body']['data']['messagingListSubscribers']['subscribers'][0]['topicId'], $subscriber['topicId']); + $this->assertEquals($response['body']['data']['messagingListSubscribers']['subscribers'][0]['targetId'], $subscriber['targetId']); + $this->assertEquals($response['body']['data']['messagingListSubscribers']['subscribers'][0]['userId'], $subscriber['userId']); $this->assertEquals(1, \count($response['body']['data']['messagingListSubscribers']['subscribers'])); } @@ -478,6 +485,9 @@ class MessagingTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals($subscriberId, $response['body']['data']['messagingGetSubscriber']['_id']); + $this->assertEquals($topicId, $response['body']['data']['messagingGetSubscriber']['topicId']); + $this->assertEquals($subscriber['targetId'], $response['body']['data']['messagingGetSubscriber']['targetId']); + $this->assertEquals($subscriber['userId'], $response['body']['data']['messagingGetSubscriber']['userId']); } /** @@ -604,6 +614,7 @@ class MessagingTest extends Scope 'query' => $query, 'variables' => [ 'targetId' => ID::unique(), + 'providerType' => 'email', 'userId' => $user['body']['data']['usersCreate']['_id'], 'providerId' => $providerId, 'identifier' => $to, @@ -678,119 +689,13 @@ class MessagingTest extends Scope */ public function testUpdateEmail(array $email) { - if (empty(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN'))) { - $this->markTestSkipped('Email DSN not provided'); - } - - $emailDSN = new DSN(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN')); - $to = $emailDSN->getParam('to'); - $from = $emailDSN->getParam('from'); - $isEuRegion = $emailDSN->getParam('isEuRegion'); - $apiKey = $emailDSN->getPassword(); - $domain = $emailDSN->getUser(); - - if (empty($to) || empty($from) || empty($apiKey) || empty($domain) || empty($isEuRegion)) { - $this->markTestSkipped('Email provider not configured'); - } - - $query = $this->getQuery(self::$CREATE_MAILGUN_PROVIDER); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'providerId' => ID::unique(), - 'name' => 'Mailgun2', - 'apiKey' => $apiKey, - 'domain' => $domain, - 'from' => $from, - 'isEuRegion' => filter_var($isEuRegion, FILTER_VALIDATE_BOOLEAN), - ], - ]; - $provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $graphQLPayload); - - $this->assertEquals(200, $provider['headers']['status-code']); - - $providerId = $provider['body']['data']['messagingCreateMailgunProvider']['_id']; - - $query = $this->getQuery(self::$CREATE_TOPIC); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'topicId' => ID::unique(), - 'name' => 'topic1', - 'description' => 'Active users', - ], - ]; - $topic = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $graphQLPayload); - - $this->assertEquals(200, $topic['headers']['status-code']); - - $query = $this->getQuery(self::$CREATE_USER); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'userId' => ID::unique(), - 'email' => 'random2-mail@mail.org', - 'password' => 'password', - 'name' => 'Messaging User', - ] - ]; - $user = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $graphQLPayload); - - $this->assertEquals(200, $user['headers']['status-code']); - - $query = $this->getQuery(self::$CREATE_USER_TARGET); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'targetId' => ID::unique(), - 'userId' => $user['body']['data']['usersCreate']['_id'], - 'providerId' => $providerId, - 'identifier' => $to, - ], - ]; - $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $graphQLPayload); - - $this->assertEquals(200, $target['headers']['status-code']); - - $query = $this->getQuery(self::$CREATE_SUBSCRIBER); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'subscriberId' => ID::unique(), - 'topicId' => $topic['body']['data']['messagingCreateTopic']['_id'], - 'targetId' => $target['body']['data']['usersCreateTarget']['_id'], - ], - ]; - $subscriber = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), $graphQLPayload); - - $this->assertEquals(200, $subscriber['headers']['status-code']); - $query = $this->getQuery(self::$CREATE_EMAIL); $graphQLPayload = [ 'query' => $query, 'variables' => [ 'messageId' => ID::unique(), 'status' => 'draft', - 'topics' => [$topic['body']['data']['messagingCreateTopic']['_id']], + 'topics' => [$email['topics'][0]], 'subject' => 'Khali beats Undertaker', 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', ], @@ -916,6 +821,7 @@ class MessagingTest extends Scope 'query' => $query, 'variables' => [ 'targetId' => ID::unique(), + 'providerType' => 'sms', 'userId' => $user['body']['data']['usersCreate']['_id'], 'providerId' => $providerId, 'identifier' => $to, @@ -988,117 +894,13 @@ class MessagingTest extends Scope */ public function testUpdateSMS(array $sms) { - if (empty(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN'))) { - $this->markTestSkipped('SMS DSN not provided'); - } - - $smsDSN = new DSN(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN')); - $to = $smsDSN->getParam('to'); - $from = $smsDSN->getParam('from'); - $authKey = $smsDSN->getPassword(); - $senderId = $smsDSN->getUser(); - - if (empty($to) || empty($from) || empty($senderId) || empty($authKey)) { - $this->markTestSkipped('SMS provider not configured'); - } - - $query = $this->getQuery(self::$CREATE_MSG91_PROVIDER); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'providerId' => ID::unique(), - 'name' => 'Msg91-2', - 'senderId' => $senderId, - 'authKey' => $authKey, - 'from' => $from, - ], - ]; - $provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $graphQLPayload); - - $this->assertEquals(200, $provider['headers']['status-code']); - - $providerId = $provider['body']['data']['messagingCreateMsg91Provider']['_id']; - - $query = $this->getQuery(self::$CREATE_TOPIC); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'topicId' => ID::unique(), - 'name' => 'topic1', - 'description' => 'Active users', - ], - ]; - $topic = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $graphQLPayload); - - $this->assertEquals(200, $topic['headers']['status-code']); - - $query = $this->getQuery(self::$CREATE_USER); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'userId' => ID::unique(), - 'email' => 'random4-email@mail.org', - 'password' => 'password', - 'name' => 'Messaging User', - ] - ]; - $user = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $graphQLPayload); - - $this->assertEquals(200, $user['headers']['status-code']); - - $query = $this->getQuery(self::$CREATE_USER_TARGET); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'targetId' => ID::unique(), - 'userId' => $user['body']['data']['usersCreate']['_id'], - 'providerId' => $providerId, - 'identifier' => $to, - ], - ]; - $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $graphQLPayload); - - $this->assertEquals(200, $target['headers']['status-code']); - - $query = $this->getQuery(self::$CREATE_SUBSCRIBER); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'subscriberId' => ID::unique(), - 'topicId' => $topic['body']['data']['messagingCreateTopic']['_id'], - 'targetId' => $target['body']['data']['usersCreateTarget']['_id'], - ], - ]; - $subscriber = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), $graphQLPayload); - - $this->assertEquals(200, $subscriber['headers']['status-code']); - $query = $this->getQuery(self::$CREATE_SMS); $graphQLPayload = [ 'query' => $query, 'variables' => [ 'messageId' => ID::unique(), 'status' => 'draft', - 'topics' => [$topic['body']['data']['messagingCreateTopic']['_id']], + 'topics' => [$sms['topics'][0]], 'content' => '345463', ], ]; @@ -1219,6 +1021,7 @@ class MessagingTest extends Scope 'query' => $query, 'variables' => [ 'targetId' => ID::unique(), + 'providerType' => 'push', 'userId' => $user['body']['data']['usersCreate']['_id'], 'providerId' => $providerId, 'identifier' => $to, @@ -1293,112 +1096,13 @@ class MessagingTest extends Scope */ public function testUpdatePushNotification(array $push) { - if (empty(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN'))) { - $this->markTestSkipped('Push DSN empty'); - } - - $pushDSN = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN')); - $to = $pushDSN->getParam('to'); - $serverKey = $pushDSN->getPassword(); - - if (empty($to) || empty($serverKey)) { - $this->markTestSkipped('Push provider not configured'); - } - - $query = $this->getQuery(self::$CREATE_FCM_PROVIDER); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'providerId' => ID::unique(), - 'name' => 'FCM2', - 'serverKey' => $serverKey, - ], - ]; - $provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $graphQLPayload); - - $this->assertEquals(200, $provider['headers']['status-code']); - $providerId = $provider['body']['data']['messagingCreateFcmProvider']['_id']; - - $query = $this->getQuery(self::$CREATE_TOPIC); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'topicId' => ID::unique(), - 'name' => 'topic1', - 'description' => 'Active users', - ], - ]; - $topic = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $graphQLPayload); - - $this->assertEquals(200, $topic['headers']['status-code']); - - $query = $this->getQuery(self::$CREATE_USER); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'userId' => ID::unique(), - 'email' => 'random5-email@mail.org', - 'password' => 'password', - 'name' => 'Messaging User', - ] - ]; - $user = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $graphQLPayload); - - $this->assertEquals(200, $user['headers']['status-code']); - - $query = $this->getQuery(self::$CREATE_USER_TARGET); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'targetId' => ID::unique(), - 'userId' => $user['body']['data']['usersCreate']['_id'], - 'providerId' => $providerId, - 'identifier' => $to, - ], - ]; - $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $graphQLPayload); - - $this->assertEquals(200, $target['headers']['status-code']); - - $query = $this->getQuery(self::$CREATE_SUBSCRIBER); - $graphQLPayload = [ - 'query' => $query, - 'variables' => [ - 'subscriberId' => ID::unique(), - 'topicId' => $topic['body']['data']['messagingCreateTopic']['_id'], - 'targetId' => $target['body']['data']['usersCreateTarget']['_id'], - ], - ]; - $subscriber = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), $graphQLPayload); - - $this->assertEquals(200, $subscriber['headers']['status-code']); - $query = $this->getQuery(self::$CREATE_PUSH_NOTIFICATION); $graphQLPayload = [ 'query' => $query, 'variables' => [ 'messageId' => ID::unique(), 'status' => 'draft', - 'topics' => [$topic['body']['data']['messagingCreateTopic']['_id']], + 'topics' => [$push['topics'][0]], 'title' => 'Push Notification Title', 'body' => 'Push Notifiaction Body', ], diff --git a/tests/e2e/Services/GraphQL/UsersTest.php b/tests/e2e/Services/GraphQL/UsersTest.php index d750de5834..59c0c4a805 100644 --- a/tests/e2e/Services/GraphQL/UsersTest.php +++ b/tests/e2e/Services/GraphQL/UsersTest.php @@ -78,6 +78,7 @@ class UsersTest extends Scope 'variables' => [ 'targetId' => ID::unique(), 'userId' => $user['_id'], + 'providerType' => 'email', 'providerId' => $providerId, 'identifier' => 'identifier', ] @@ -479,7 +480,7 @@ class UsersTest extends Scope ], $this->getHeaders()), $graphQLPayload); $this->assertEquals(200, $target['headers']['status-code']); - $this->assertEquals('newidentifier', $target['body']['data']['usersUpdateTargetIdentifier']['identifier']); + $this->assertEquals('newidentifier', $target['body']['data']['usersUpdateTarget']['identifier']); } public function testDeleteUserSessions() diff --git a/tests/e2e/Services/Health/HealthCustomServerTest.php b/tests/e2e/Services/Health/HealthCustomServerTest.php index eafc961e8b..8fa9faadd2 100644 --- a/tests/e2e/Services/Health/HealthCustomServerTest.php +++ b/tests/e2e/Services/Health/HealthCustomServerTest.php @@ -138,6 +138,15 @@ class HealthCustomServerTest extends Scope $this->assertIsInt($response['body']['size']); $this->assertLessThan(100, $response['body']['size']); + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/webhooks?threshold=0', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + $this->assertEquals(500, $response['headers']['status-code']); + return []; } @@ -155,6 +164,15 @@ class HealthCustomServerTest extends Scope $this->assertIsInt($response['body']['size']); $this->assertLessThan(100, $response['body']['size']); + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/logs?threshold=0', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + $this->assertEquals(500, $response['headers']['status-code']); + return []; } @@ -172,6 +190,15 @@ class HealthCustomServerTest extends Scope $this->assertIsInt($response['body']['size']); $this->assertLessThan(100, $response['body']['size']); + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/certificates?threshold=0', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + $this->assertEquals(500, $response['headers']['status-code']); + return []; } @@ -189,6 +216,15 @@ class HealthCustomServerTest extends Scope $this->assertIsInt($response['body']['size']); $this->assertLessThan(100, $response['body']['size']); + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/functions?threshold=0', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + $this->assertEquals(500, $response['headers']['status-code']); + return []; } @@ -206,6 +242,15 @@ class HealthCustomServerTest extends Scope $this->assertIsInt($response['body']['size']); $this->assertLessThan(100, $response['body']['size']); + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/builds?threshold=0', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + $this->assertEquals(500, $response['headers']['status-code']); + return []; } @@ -225,6 +270,18 @@ class HealthCustomServerTest extends Scope $this->assertIsInt($response['body']['size']); $this->assertLessThan(100, $response['body']['size']); + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'database_db_main', + 'threshold' => '0' + ]); + $this->assertEquals(500, $response['headers']['status-code']); + return []; } @@ -242,6 +299,15 @@ class HealthCustomServerTest extends Scope $this->assertIsInt($response['body']['size']); $this->assertLessThan(100, $response['body']['size']); + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/deletes?threshold=0', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + $this->assertEquals(500, $response['headers']['status-code']); + return []; } @@ -259,6 +325,15 @@ class HealthCustomServerTest extends Scope $this->assertIsInt($response['body']['size']); $this->assertLessThan(100, $response['body']['size']); + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/mails?threshold=0', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + $this->assertEquals(500, $response['headers']['status-code']); + return []; } @@ -276,6 +351,15 @@ class HealthCustomServerTest extends Scope $this->assertIsInt($response['body']['size']); $this->assertLessThan(100, $response['body']['size']); + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/messaging?threshold=0', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + $this->assertEquals(500, $response['headers']['status-code']); + return []; } @@ -293,6 +377,15 @@ class HealthCustomServerTest extends Scope $this->assertIsInt($response['body']['size']); $this->assertLessThan(100, $response['body']['size']); + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/migrations?threshold=0', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + $this->assertEquals(500, $response['headers']['status-code']); + return []; } diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php index 78f2330b96..1f09d95522 100644 --- a/tests/e2e/Services/Messaging/MessagingBase.php +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -317,9 +317,11 @@ trait MessagingBase 'x-appwrite-key' => $this->getProject()['apiKey'], ]), [ 'targetId' => ID::unique(), + 'providerType' => 'email', 'providerId' => $provider['body']['$id'], 'identifier' => 'my-token', ]); + $this->assertEquals(201, $target['headers']['status-code']); $response = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['$id'] . '/subscribers', \array_merge([ @@ -329,13 +331,16 @@ trait MessagingBase 'subscriberId' => ID::unique(), 'targetId' => $target['body']['$id'], ]); + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals($target['body']['userId'], $response['body']['userId']); $topic = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['$id'], [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]); + $this->assertEquals(200, $topic['headers']['status-code']); $this->assertEquals('android-app', $topic['body']['name']); $this->assertEquals('updated-description', $topic['body']['description']); @@ -344,6 +349,7 @@ trait MessagingBase return [ 'topicId' => $topic['body']['$id'], 'targetId' => $target['body']['$id'], + 'userId' => $target['body']['userId'], 'subscriberId' => $response['body']['$id'] ]; } @@ -353,14 +359,16 @@ trait MessagingBase */ public function testGetSubscriber(array $data) { - $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $data['topicId'] . '/subscriber/' . $data['subscriberId'], \array_merge([ + $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $data['topicId'] . '/subscribers/' . $data['subscriberId'], \array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ])); + $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals($data['topicId'], $response['body']['topicId']); $this->assertEquals($data['targetId'], $response['body']['targetId']); + $this->assertEquals($data['userId'], $response['body']['userId']); } /** @@ -376,6 +384,7 @@ trait MessagingBase $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(1, $response['body']['total']); + $this->assertEquals($data['userId'], $response['body']['subscribers'][0]['userId']); $this->assertEquals(\count($response['body']['subscribers']), $response['body']['total']); return $data; @@ -496,7 +505,7 @@ trait MessagingBase */ public function testDeleteSubscriber(array $data) { - $response = $this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $data['topicId'] . '/subscriber/' . $data['subscriberId'], \array_merge([ + $response = $this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $data['topicId'] . '/subscribers/' . $data['subscriberId'], \array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); @@ -558,6 +567,7 @@ trait MessagingBase 'isEuRegion' => filter_var($isEuRegion, FILTER_VALIDATE_BOOLEAN), 'from' => $from ]); + $this->assertEquals(201, $provider['headers']['status-code']); // Create Topic @@ -570,6 +580,7 @@ trait MessagingBase 'name' => 'topic1', 'description' => 'Test Topic' ]); + $this->assertEquals(201, $topic['headers']['status-code']); // Create User @@ -593,6 +604,7 @@ trait MessagingBase 'x-appwrite-key' => $this->getProject()['apiKey'], ], [ 'targetId' => ID::unique(), + 'providerType' => 'email', 'providerId' => $provider['body']['$id'], 'identifier' => $to, ]); @@ -645,21 +657,6 @@ trait MessagingBase */ public function testUpdateEmail(array $email) { - if (empty(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN'))) { - $this->markTestSkipped('Email DSN not provided'); - } - - $emailDSN = new DSN(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN')); - $to = $emailDSN->getParam('to'); - $from = $emailDSN->getParam('from'); - $isEuRegion = $emailDSN->getParam('isEuRegion'); - $apiKey = $emailDSN->getPassword(); - $domain = $emailDSN->getUser(); - - if (empty($to) || empty($from) || empty($apiKey) || empty($domain) || empty($isEuRegion)) { - $this->markTestSkipped('Email provider not configured'); - } - $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $email['body']['$id'], [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -669,71 +666,6 @@ trait MessagingBase // Test failure as the message has already been sent. $this->assertEquals(400, $message['headers']['status-code']); - // Create provider - $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/mailgun', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), [ - 'providerId' => ID::unique(), - 'name' => 'Mailgun-provider-2', - 'apiKey' => $apiKey, - 'domain' => $domain, - 'isEuRegion' => filter_var($isEuRegion, FILTER_VALIDATE_BOOLEAN), - 'from' => $from - ]); - $this->assertEquals(201, $provider['headers']['status-code']); - - // Create Topic - $topic = $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' => ID::unique(), - 'name' => 'topic1', - 'description' => 'Test Topic' - ]); - $this->assertEquals(201, $topic['headers']['status-code']); - - // Create User - $user = $this->client->call(Client::METHOD_POST, '/users', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'userId' => ID::unique(), - 'email' => 'random-email@mail.org', - 'password' => 'password', - 'name' => 'Messaging User', - ]); - - $this->assertEquals(201, $user['headers']['status-code']); - - // Create Target - $target = $this->client->call(Client::METHOD_POST, '/users/' . $user['body']['$id'] . '/targets', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'targetId' => ID::unique(), - 'providerId' => $provider['body']['$id'], - 'identifier' => $to, - ]); - - $this->assertEquals(201, $target['headers']['status-code']); - - // Create Subscriber - $subscriber = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['body']['$id'] . '/subscribers', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'subscriberId' => ID::unique(), - 'targetId' => $target['body']['$id'], - ]); - - $this->assertEquals(201, $subscriber['headers']['status-code']); - // Create Email $email = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [ 'content-type' => 'application/json', @@ -742,7 +674,7 @@ trait MessagingBase ], [ 'messageId' => ID::unique(), 'status' => 'draft', - 'topics' => [$topic['body']['$id']], + 'topics' => [$email['body']['topics'][0]], 'subject' => 'Khali beats Undertaker', 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', ]); @@ -801,6 +733,7 @@ trait MessagingBase 'authKey' => $authKey, 'from' => $from ]); + $this->assertEquals(201, $provider['headers']['status-code']); // Create Topic @@ -813,6 +746,7 @@ trait MessagingBase 'name' => 'topic1', 'description' => 'Test Topic' ]); + $this->assertEquals(201, $topic['headers']['status-code']); // Create User @@ -836,6 +770,7 @@ trait MessagingBase 'x-appwrite-key' => $this->getProject()['apiKey'], ], [ 'targetId' => ID::unique(), + 'providerType' => 'sms', 'providerId' => $provider['body']['$id'], 'identifier' => $to, ]); @@ -887,20 +822,6 @@ trait MessagingBase */ public function testUpdateSMS(array $sms) { - if (empty(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN'))) { - $this->markTestSkipped('SMS DSN not provided'); - } - - $smsDSN = new DSN(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN')); - $to = $smsDSN->getParam('to'); - $from = $smsDSN->getParam('from'); - $authKey = $smsDSN->getPassword(); - $senderId = $smsDSN->getUser(); - - if (empty($to) || empty($from) || empty($senderId) || empty($authKey)) { - $this->markTestSkipped('SMS provider not configured'); - } - $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/sms/' . $sms['body']['$id'], [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -910,70 +831,6 @@ trait MessagingBase // Test failure as the message has already been sent. $this->assertEquals(400, $message['headers']['status-code']); - // Create provider - $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/msg91', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), [ - 'providerId' => ID::unique(), - 'name' => 'Msg91-2', - 'senderId' => $senderId, - 'authKey' => $authKey, - 'from' => $from - ]); - $this->assertEquals(201, $provider['headers']['status-code']); - - // Create Topic - $topic = $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' => ID::unique(), - 'name' => 'topic1', - 'description' => 'Test Topic' - ]); - $this->assertEquals(201, $topic['headers']['status-code']); - - // Create User - $user = $this->client->call(Client::METHOD_POST, '/users', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'userId' => ID::unique(), - 'email' => 'random2-email@mail.org', - 'password' => 'password', - 'name' => 'Messaging User', - ]); - - $this->assertEquals(201, $user['headers']['status-code']); - - // Create Target - $target = $this->client->call(Client::METHOD_POST, '/users/' . $user['body']['$id'] . '/targets', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'targetId' => ID::unique(), - 'providerId' => $provider['body']['$id'], - 'identifier' => $to, - ]); - - $this->assertEquals(201, $target['headers']['status-code']); - - // Create Subscriber - $subscriber = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['body']['$id'] . '/subscribers', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'subscriberId' => ID::unique(), - 'targetId' => $target['body']['$id'], - ]); - - $this->assertEquals(201, $subscriber['headers']['status-code']); - // Create SMS $sms = $this->client->call(Client::METHOD_POST, '/messaging/messages/sms', [ 'content-type' => 'application/json', @@ -982,7 +839,7 @@ trait MessagingBase ], [ 'messageId' => ID::unique(), 'status' => 'draft', - 'topics' => [$topic['body']['$id']], + 'topics' => [$sms['body']['topics'][0]], 'content' => '047487', ]); @@ -1036,6 +893,7 @@ trait MessagingBase 'name' => 'FCM-1', 'serverKey' => $serverKey, ]); + $this->assertEquals(201, $provider['headers']['status-code']); // Create Topic @@ -1048,6 +906,7 @@ trait MessagingBase 'name' => 'topic1', 'description' => 'Test Topic' ]); + $this->assertEquals(201, $topic['headers']['status-code']); // Create User @@ -1071,6 +930,7 @@ trait MessagingBase 'x-appwrite-key' => $this->getProject()['apiKey'], ], [ 'targetId' => ID::unique(), + 'providerType' => 'push', 'providerId' => $provider['body']['$id'], 'identifier' => $to, ]); @@ -1123,18 +983,6 @@ trait MessagingBase */ public function testUpdatePushNotification(array $push) { - if (empty(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN'))) { - $this->markTestSkipped('Push DSN empty'); - } - - $pushDSN = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN')); - $to = $pushDSN->getParam('to'); - $serverKey = $pushDSN->getPassword(); - - if (empty($to) || empty($serverKey)) { - $this->markTestSkipped('Push provider not configured'); - } - $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/push/' . $push['body']['$id'], [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1144,68 +992,6 @@ trait MessagingBase // Test failure as the message has already been sent. $this->assertEquals(400, $message['headers']['status-code']); - // Create provider - $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/fcm', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), [ - 'providerId' => ID::unique(), - 'name' => 'FCM-2', - 'serverKey' => $serverKey, - ]); - $this->assertEquals(201, $provider['headers']['status-code']); - - // Create Topic - $topic = $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' => ID::unique(), - 'name' => 'topic1', - 'description' => 'Test Topic' - ]); - $this->assertEquals(201, $topic['headers']['status-code']); - - // Create User - $user = $this->client->call(Client::METHOD_POST, '/users', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'userId' => ID::unique(), - 'email' => 'random4-email@mail.org', - 'password' => 'password', - 'name' => 'Messaging User', - ]); - - $this->assertEquals(201, $user['headers']['status-code']); - - // Create Target - $target = $this->client->call(Client::METHOD_POST, '/users/' . $user['body']['$id'] . '/targets', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'targetId' => ID::unique(), - 'providerId' => $provider['body']['$id'], - 'identifier' => $to, - ]); - - $this->assertEquals(201, $target['headers']['status-code']); - - // Create Subscriber - $subscriber = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['body']['$id'] . '/subscribers', \array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'subscriberId' => ID::unique(), - 'targetId' => $target['body']['$id'], - ]); - - $this->assertEquals(201, $subscriber['headers']['status-code']); - // Create push notification $push = $this->client->call(Client::METHOD_POST, '/messaging/messages/push', [ 'content-type' => 'application/json', @@ -1214,7 +1000,7 @@ trait MessagingBase ], [ 'messageId' => ID::unique(), 'status' => 'draft', - 'topics' => [$topic['body']['$id']], + 'topics' => [$push['body']['topics'][0]], 'title' => 'Test-Notification', 'body' => 'Test-Notification-Body', ]); diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php index 9ca00aa5df..c9cb17d7b5 100644 --- a/tests/e2e/Services/Users/UsersBase.php +++ b/tests/e2e/Services/Users/UsersBase.php @@ -1232,7 +1232,7 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'providerId' => 'unique()', + 'providerId' => ID::unique(), 'name' => 'Sengrid1', 'apiKey' => 'my-apikey', 'from' => 'from@domain.com', @@ -1244,6 +1244,7 @@ trait UsersBase ], $this->getHeaders()), [ 'targetId' => ID::unique(), 'providerId' => $provider['body']['$id'], + 'providerType' => 'email', 'identifier' => 'my-token', ]); $this->assertEquals(201, $response['headers']['status-code']); @@ -1257,7 +1258,7 @@ trait UsersBase */ public function testUpdateUserTarget(array $data): array { - $response = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/targets/' . $data['$id'] . '/identifier', array_merge([ + $response = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/targets/' . $data['$id'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -1303,11 +1304,14 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); + $this->assertEquals(204, $response['headers']['status-code']); + $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); + $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(0, $response['body']['total']); } diff --git a/tests/e2e/Services/Webhooks/WebhooksCustomClientTest.php b/tests/e2e/Services/Webhooks/WebhooksCustomClientTest.php index 67c2dec36b..21c270d199 100644 --- a/tests/e2e/Services/Webhooks/WebhooksCustomClientTest.php +++ b/tests/e2e/Services/Webhooks/WebhooksCustomClientTest.php @@ -636,6 +636,120 @@ class WebhooksCustomClientTest extends Scope return $data; } + /** + * @depends testUpdateAccountPrefs + */ + public function testCreateAccountVerification($data): array + { + $id = $data['id'] ?? ''; + $email = $data['email'] ?? ''; + $session = $data['session'] ?? ''; + + $verification = $this->client->call(Client::METHOD_POST, '/account/verification', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ]), [ + 'url' => 'http://localhost/verification', + ]); + + $verificationId = $verification['body']['$id']; + + $this->assertEquals(201, $verification['headers']['status-code']); + $this->assertIsArray($verification['body']); + + $webhook = $this->getLastRequest(); + $signatureKey = $this->getProject()['signatureKey']; + $payload = json_encode($webhook['data']); + $url = $webhook['url']; + $signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true)); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('users.*.verification.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('users.*.verification.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.*.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.*.verification.{$verificationId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}.verification.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}.verification.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}.verification.{$verificationId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide())); + $this->assertNotEmpty($webhook['data']['$id']); + $this->assertNotEmpty($webhook['data']['userId']); + $this->assertNotEmpty($webhook['data']['secret']); + $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['expire'])); + + $data['secret'] = $webhook['data']['secret']; + + return $data; + } + + /** + * @depends testCreateAccountVerification + */ + public function testUpdateAccountVerification($data): array + { + $id = $data['id'] ?? ''; + $email = $data['email'] ?? ''; + $session = $data['session'] ?? ''; + $secret = $data['secret'] ?? ''; + + $verification = $this->client->call(Client::METHOD_PUT, '/account/verification', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ]), [ + 'userId' => $id, + 'secret' => $secret, + ]); + + $verificationId = $verification['body']['$id']; + + $this->assertEquals(200, $verification['headers']['status-code']); + $this->assertIsArray($verification['body']); + + $webhook = $this->getLastRequest(); + $signatureKey = $this->getProject()['signatureKey']; + $payload = json_encode($webhook['data']); + $url = $webhook['url']; + $signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true)); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('users.*.verification.*', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString('users.*.verification.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.*.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.*.verification.{$verificationId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}.verification.*", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}.verification.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertStringContainsString("users.{$id}.verification.{$verificationId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide())); + $this->assertNotEmpty($webhook['data']['$id']); + $this->assertNotEmpty($webhook['data']['userId']); + $this->assertNotEmpty($webhook['data']['secret']); + $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['expire'])); + + $data['secret'] = $webhook['data']['secret']; + + return $data; + } + /** * @depends testUpdateAccountPrefs */ @@ -751,120 +865,6 @@ class WebhooksCustomClientTest extends Scope return $data; } - /** - * @depends testUpdateAccountPrefs - */ - public function testCreateAccountVerification($data): array - { - $id = $data['id'] ?? ''; - $email = $data['email'] ?? ''; - $session = $data['session'] ?? ''; - - $verification = $this->client->call(Client::METHOD_POST, '/account/verification', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, - ]), [ - 'url' => 'http://localhost/verification', - ]); - - $verificationId = $verification['body']['$id']; - - $this->assertEquals(201, $verification['headers']['status-code']); - $this->assertIsArray($verification['body']); - - $webhook = $this->getLastRequest(); - $signatureKey = $this->getProject()['signatureKey']; - $payload = json_encode($webhook['data']); - $url = $webhook['url']; - $signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true)); - - $this->assertEquals($webhook['method'], 'POST'); - $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); - $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); - $this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('users.*.verification.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('users.*.verification.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.*.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.*.verification.{$verificationId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}.verification.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}.verification.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}.verification.{$verificationId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected); - $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); - $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); - $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide())); - $this->assertNotEmpty($webhook['data']['$id']); - $this->assertNotEmpty($webhook['data']['userId']); - $this->assertNotEmpty($webhook['data']['secret']); - $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['expire'])); - - $data['secret'] = $webhook['data']['secret']; - - return $data; - } - - /** - * @depends testCreateAccountVerification - */ - public function testUpdateAccountVerification($data): array - { - $id = $data['id'] ?? ''; - $email = $data['email'] ?? ''; - $session = $data['session'] ?? ''; - $secret = $data['secret'] ?? ''; - - $verification = $this->client->call(Client::METHOD_PUT, '/account/verification', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, - ]), [ - 'userId' => $id, - 'secret' => $secret, - ]); - - $verificationId = $verification['body']['$id']; - - $this->assertEquals(200, $verification['headers']['status-code']); - $this->assertIsArray($verification['body']); - - $webhook = $this->getLastRequest(); - $signatureKey = $this->getProject()['signatureKey']; - $payload = json_encode($webhook['data']); - $url = $webhook['url']; - $signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true)); - - $this->assertEquals($webhook['method'], 'POST'); - $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); - $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); - $this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('users.*.verification.*', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString('users.*.verification.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.*.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.*.verification.{$verificationId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}.verification.*", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}.verification.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertStringContainsString("users.{$id}.verification.{$verificationId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']); - $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected); - $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); - $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); - $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide())); - $this->assertNotEmpty($webhook['data']['$id']); - $this->assertNotEmpty($webhook['data']['userId']); - $this->assertNotEmpty($webhook['data']['secret']); - $this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['expire'])); - - $data['secret'] = $webhook['data']['secret']; - - return $data; - } - /** * @depends testCreateTeamMembership */