From a282ab38a6f42a718ce7c2132f134f55450d2026 Mon Sep 17 00:00:00 2001 From: prateek banga Date: Thu, 16 Nov 2023 02:12:06 +0530 Subject: [PATCH 1/6] adds target endpoint in account controller for push tokensl --- app/config/collections.php | 11 +++ app/controllers/api/account.php | 153 ++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) diff --git a/app/config/collections.php b/app/config/collections.php index 0d42dfe895..0c67cd175f 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1870,6 +1870,17 @@ $commonCollections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('name'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['json'], + ], ], 'indexes' => [ [ diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 6486147a84..71a5562ae3 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1677,6 +1677,96 @@ App::post('/v1/account/jwt') ])]), Response::MODEL_JWT); }); +App::post('/v1/account/targets') + ->desc('Create Account 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('scope', 'public') + ->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('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) + ->param('name', '', new Text(256), 'Set Client device name manually instead of using user agent. Used to identify the client device make, model, OS, etc.', true) + ->inject('queueForEvents') + ->inject('user') + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $targetId, string $providerType, string $identifier, string $providerId, string $name, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { + $targetId = $targetId == 'unique()' ? ID::unique() : $targetId; + + $provider = new Document(); + + if ($providerType === 'push') { + $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) + + $os = $detector->getOS(); + $client = $detector->getClient(); + $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' => $providerType, + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'identifier' => $identifier, + 'name' => $name ?? [ + 'osCode' => $os['osCode'], + 'osName' => $os['osName'], + 'osVersion' => $os['osVersion'], + 'clientType' => $client['clientType'], + 'clientCode' => $client['clientCode'], + 'clientName' => $client['clientName'], + 'clientVersion' => $client['clientVersion'], + 'clientEngine' => $client['clientEngine'], + 'clientEngineVersion' => $client['clientEngineVersion'], + 'deviceName' => $device['deviceName'], + 'deviceBrand' => $device['deviceBrand'], + 'deviceModel' => $device['deviceModel'] + ] + ])); + } catch (Duplicate) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + Authorization::skip(fn () => $dbForProject->deleteCachedDocument('users', $user->getId())); + + $queueForEvents + ->setParam('userId', $user->getId()) + ->setParam('targetId', $target->getId()); + + $response + ->dynamic($target, Response::MODEL_TARGET); + }); + App::get('/v1/account') ->desc('Get account') ->groups(['api', 'account']) @@ -3077,3 +3167,66 @@ App::put('/v1/account/verification/phone') $response->dynamic($verificationDocument, Response::MODEL_TOKEN); }); + +App::put('/v1/account/targets/:targetId') + ->desc('Update Account 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].create') + ->label('scope', 'public') + ->label('docs', false) + ->param('targetId', '', new UID(), 'Target ID.') + ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)', true) + ->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('name', '', new Text(256), 'Set Client device name manually instead of using user agent. Used to identify the client device make, model, OS, etc.', true) + ->inject('queueForEvents') + ->inject('user') + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $targetId, string $identifier, string $providerId, string $name, 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); + } + + 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()); + } + + if ($name) { + $target->setAttribute('name', $name); + } + + $target = $dbForProject->updateDocument('targets', $target->getId(), $target); + Authorization::skip(fn () => $dbForProject->deleteCachedDocument('users', $user->getId())); + + $queueForEvents + ->setParam('userId', $user->getId()) + ->setParam('targetId', $target->getId()); + + $response + ->dynamic($target, Response::MODEL_TARGET); + }); From c1869bb0caf62cf89829d2e495981df4a86d72e7 Mon Sep 17 00:00:00 2001 From: prateek banga Date: Thu, 16 Nov 2023 11:20:39 +0530 Subject: [PATCH 2/6] adds messaging in test workflow --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) 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: From f85462270584982d4b928a8fa2abb2493973ea93 Mon Sep 17 00:00:00 2001 From: prateek banga Date: Thu, 16 Nov 2023 16:26:36 +0530 Subject: [PATCH 3/6] review changes --- app/controllers/api/account.php | 63 ++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 71a5562ae3..154e385c76 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1677,36 +1677,35 @@ App::post('/v1/account/jwt') ])]), Response::MODEL_JWT); }); -App::post('/v1/account/targets') +App::post('/v1/account/targets/push') ->desc('Create Account 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('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'createTarget') + ->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('providerType', '', new WhiteList(['email', 'sms', 'push']), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.') + ->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.)') - ->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('name', '', new Text(256), 'Set Client device name manually instead of using user agent. Used to identify the client device make, model, OS, etc.', true) ->inject('queueForEvents') ->inject('user') ->inject('request') ->inject('response') ->inject('dbForProject') - ->action(function (string $targetId, string $providerType, string $identifier, string $providerId, string $name, Event $queueForEvents, Document $user, Request $request, Response $response, Database $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 = new Document(); + $provider = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId)); - if ($providerType === 'push') { - $provider = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId)); - - if ($provider->isEmpty()) { - throw new Exception(Exception::PROVIDER_NOT_FOUND); - } + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); } if ($user->isEmpty()) { @@ -1735,11 +1734,11 @@ App::post('/v1/account/targets') ], 'providerId' => $providerId ?? null, 'providerInternalId' => $provider->getInternalId() ?? null, - 'providerType' => $providerType, + 'providerType' => 'push', 'userId' => $user->getId(), 'userInternalId' => $user->getInternalId(), 'identifier' => $identifier, - 'name' => $name ?? [ + 'name' => [ 'osCode' => $os['osCode'], 'osName' => $os['osName'], 'osVersion' => $os['osVersion'], @@ -1749,7 +1748,6 @@ App::post('/v1/account/targets') 'clientVersion' => $client['clientVersion'], 'clientEngine' => $client['clientEngine'], 'clientEngineVersion' => $client['clientEngineVersion'], - 'deviceName' => $device['deviceName'], 'deviceBrand' => $device['deviceBrand'], 'deviceModel' => $device['deviceModel'] ] @@ -1757,13 +1755,14 @@ App::post('/v1/account/targets') } catch (Duplicate) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } - Authorization::skip(fn () => $dbForProject->deleteCachedDocument('users', $user->getId())); + $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); }); @@ -3168,7 +3167,7 @@ App::put('/v1/account/verification/phone') $response->dynamic($verificationDocument, Response::MODEL_TOKEN); }); -App::put('/v1/account/targets/:targetId') +App::put('/v1/account/targets/:targetId/push') ->desc('Update Account Target') ->groups(['api', 'account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') @@ -3180,13 +3179,12 @@ App::put('/v1/account/targets/:targetId') ->param('targetId', '', new UID(), 'Target ID.') ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)', true) ->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('name', '', new Text(256), 'Set Client device name manually instead of using user agent. Used to identify the client device make, model, OS, etc.', true) ->inject('queueForEvents') ->inject('user') ->inject('request') ->inject('response') ->inject('dbForProject') - ->action(function (string $targetId, string $identifier, string $providerId, string $name, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { + ->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { if ($user->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } @@ -3216,12 +3214,29 @@ App::put('/v1/account/targets/:targetId') $target->setAttribute('providerInternalId', $provider->getInternalId()); } - if ($name) { - $target->setAttribute('name', $name); - } + $detector = new Detector($request->getUserAgent()); + $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + + $os = $detector->getOS(); + $client = $detector->getClient(); + $device = $detector->getDevice(); + + $target->setAttribute('name', [ + 'osCode' => $os['osCode'], + 'osName' => $os['osName'], + 'osVersion' => $os['osVersion'], + 'clientType' => $client['clientType'], + 'clientCode' => $client['clientCode'], + 'clientName' => $client['clientName'], + 'clientVersion' => $client['clientVersion'], + 'clientEngine' => $client['clientEngine'], + 'clientEngineVersion' => $client['clientEngineVersion'], + 'deviceBrand' => $device['deviceBrand'], + 'deviceModel' => $device['deviceModel'] + ]); $target = $dbForProject->updateDocument('targets', $target->getId(), $target); - Authorization::skip(fn () => $dbForProject->deleteCachedDocument('users', $user->getId())); + $dbForProject->deleteCachedDocument('users', $user->getId()); $queueForEvents ->setParam('userId', $user->getId()) From bf95736da0db399588ba6f028254bab63f09c787 Mon Sep 17 00:00:00 2001 From: prateek banga Date: Thu, 16 Nov 2023 16:47:36 +0530 Subject: [PATCH 4/6] review changes --- app/controllers/api/account.php | 61 +++++++-------------------------- 1 file changed, 13 insertions(+), 48 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 154e385c76..45beab5ec6 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1678,7 +1678,7 @@ App::post('/v1/account/jwt') }); App::post('/v1/account/targets/push') - ->desc('Create Account Target') + ->desc('Create Account\'s push target') ->groups(['api', 'account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') ->label('audits.event', 'target.create') @@ -1686,7 +1686,7 @@ App::post('/v1/account/targets/push') ->label('event', 'users.[userId].targets.[targetId].create') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION]) ->label('sdk.namespace', 'account') - ->label('sdk.method', 'createTarget') + ->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) @@ -1721,8 +1721,6 @@ App::post('/v1/account/targets/push') $detector = new Detector($request->getUserAgent()); $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - $os = $detector->getOS(); - $client = $detector->getClient(); $device = $detector->getDevice(); try { @@ -1738,19 +1736,7 @@ App::post('/v1/account/targets/push') 'userId' => $user->getId(), 'userInternalId' => $user->getInternalId(), 'identifier' => $identifier, - 'name' => [ - 'osCode' => $os['osCode'], - 'osName' => $os['osName'], - 'osVersion' => $os['osVersion'], - 'clientType' => $client['clientType'], - 'clientCode' => $client['clientCode'], - 'clientName' => $client['clientName'], - 'clientVersion' => $client['clientVersion'], - 'clientEngine' => $client['clientEngine'], - 'clientEngineVersion' => $client['clientEngineVersion'], - 'deviceBrand' => $device['deviceBrand'], - 'deviceModel' => $device['deviceModel'] - ] + 'name' => "{$device['deviceBrand']} {$device['deviceModel']}" ])); } catch (Duplicate) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); @@ -3168,23 +3154,27 @@ App::put('/v1/account/verification/phone') }); App::put('/v1/account/targets/:targetId/push') - ->desc('Update Account Target') + ->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].create') - ->label('scope', 'public') + ->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.)', true) - ->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('user') ->inject('request') ->inject('response') ->inject('dbForProject') - ->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, Document $user, Request $request, Response $response, Database $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); } @@ -3203,37 +3193,12 @@ App::put('/v1/account/targets/:targetId/push') $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()); - } - $detector = new Detector($request->getUserAgent()); $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - $os = $detector->getOS(); - $client = $detector->getClient(); $device = $detector->getDevice(); - $target->setAttribute('name', [ - 'osCode' => $os['osCode'], - 'osName' => $os['osName'], - 'osVersion' => $os['osVersion'], - 'clientType' => $client['clientType'], - 'clientCode' => $client['clientCode'], - 'clientName' => $client['clientName'], - 'clientVersion' => $client['clientVersion'], - 'clientEngine' => $client['clientEngine'], - 'clientEngineVersion' => $client['clientEngineVersion'], - 'deviceBrand' => $device['deviceBrand'], - 'deviceModel' => $device['deviceModel'] - ]); + $target->setAttribute('name', "{$device['deviceBrand']} {$device['deviceModel']}"); $target = $dbForProject->updateDocument('targets', $target->getId(), $target); $dbForProject->deleteCachedDocument('users', $user->getId()); From 7536d8eb0feb3f1c097941385a4f3e7b1e85c0c6 Mon Sep 17 00:00:00 2001 From: prateek banga Date: Thu, 16 Nov 2023 17:09:08 +0530 Subject: [PATCH 5/6] review changes --- app/config/collections.php | 4 ++-- app/controllers/api/account.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index 0c67cd175f..0596faece7 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1874,12 +1874,12 @@ $commonCollections = [ '$id' => ID::custom('name'), 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 16384, + 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, - 'filters' => ['json'], + 'filters' => [''], ], ], 'indexes' => [ diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 45beab5ec6..783b8151a4 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -3168,7 +3168,7 @@ App::put('/v1/account/targets/:targetId/push') ->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.)', true) + ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)') ->inject('queueForEvents') ->inject('user') ->inject('request') From f0247930dab35a47e9f1534967cf8598ef20cd32 Mon Sep 17 00:00:00 2001 From: Prateek Banga <30731059+fanatic75@users.noreply.github.com> Date: Thu, 16 Nov 2023 17:15:08 +0530 Subject: [PATCH 6/6] Update app/config/collections.php Co-authored-by: Jake Barnby --- app/config/collections.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/collections.php b/app/config/collections.php index 0596faece7..7e715baa2c 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1879,7 +1879,7 @@ $commonCollections = [ 'required' => false, 'default' => null, 'array' => false, - 'filters' => [''], + 'filters' => [], ], ], 'indexes' => [