From 2e154c06bcdd0619adc09d5ad3cac927983485cb Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 9 Jan 2023 05:46:02 +0000 Subject: [PATCH 01/18] different test and messaging integration as well --- app/controllers/api/teams.php | 65 ++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 567560c6a8..66d3e0ffb8 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1,6 +1,7 @@ desc('Create Team') @@ -304,7 +306,9 @@ App::post('/v1/teams/:teamId/memberships') ->label('sdk.response.model', Response::MODEL_MEMBERSHIP) ->label('abuse-limit', 10) ->param('teamId', '', new UID(), 'Team ID.') - ->param('email', '', new Email(), 'Email of the new team member.') + ->param('userId', '', new UID(), 'User ID.', true) + ->param('email', '', new Email(), 'Email of the new team member.', true) + ->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) ->param('roles', [], new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.') ->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) // TODO add our own built-in confirm page ->param('name', '', new Text(128), 'Name of the new team member. Max length: 128 chars.', true) @@ -314,9 +318,13 @@ App::post('/v1/teams/:teamId/memberships') ->inject('dbForProject') ->inject('locale') ->inject('mails') + ->inject('messaging') ->inject('events') - ->action(function (string $teamId, string $email, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $mails, Event $events) { + ->action(function (string $teamId, string $userId, string $email, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $mails, EventPhone $messaging, Event $events) { + if(empty($userId) && empty($email) && empty($phone)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'At least one of userId, email, or phone is required'); + } $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); $isAppUser = Auth::isAppUser(Authorization::getRoles()); @@ -332,7 +340,28 @@ App::post('/v1/teams/:teamId/memberships') throw new Exception(Exception::TEAM_NOT_FOUND); } - $invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address + if(!empty($userId)) { + $invitee = $dbForProject->getDocument('users', $userId); + if($invitee->isEmpty()) { + throw new Exception(Exception::USER_NOT_FOUND, 'User with given userId doesn\'t exist.', 404); + } + if(!empty($email) && $invitee->getAttribute('email', '') != $email) { + throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given userId and email doesn\'t match', 409); + } + if(!empty($phone) && $invitee->getAttribute('phone', '') != $phone) { + throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given userId and phone doesn\'t match', 409); + } + } else if(!empty($email)) { + $invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address + if(!$invitee->isEmpty() && !empty($phone) && $invitee->getAttribute('phone', '') != $phone) { + throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given email and phone doesn\'t match', 409); + } + }else if(!empty($phone)) { + $invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]); + if(!$invitee->isEmpty() && !empty($email) && $invitee->getAttribute('email', '') != $email) { + throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409); + } + } if (empty($invitee)) { // Create new user if no user with same email found $limit = $project->getAttribute('auths', [])['limit'] ?? 0; @@ -356,6 +385,7 @@ App::post('/v1/teams/:teamId/memberships') Permission::delete(Role::user($userId)), ], 'email' => $email, + 'phone' => $phone, 'emailVerification' => false, 'status' => true, 'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS), @@ -434,16 +464,25 @@ App::post('/v1/teams/:teamId/memberships') $url = Template::unParseURL($url); if (!$isPrivilegedUser && !$isAppUser) { // No need of confirmation when in admin or app mode - $mails - ->setType(MAIL_TYPE_INVITATION) - ->setRecipient($email) - ->setUrl($url) - ->setName($name) - ->setLocale($locale->default) - ->setTeam($team) - ->setUser($user) - ->trigger() - ; + if(!empty($email)) { + $mails + ->setType(MAIL_TYPE_INVITATION) + ->setRecipient($email) + ->setUrl($url) + ->setName($name) + ->setLocale($locale->default) + ->setTeam($team) + ->setUser($user) + ->trigger() + ; + } + + if(@empty($phone)) { + $messaging + ->setRecipient($phone) + ->setMessage($url) + ->trigger(); + } } $events From a812bfb6863eae94dc5479c9cab2c7dba96d7ad6 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 9 Jan 2023 05:55:07 +0000 Subject: [PATCH 02/18] format fix --- app/controllers/api/teams.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 66d3e0ffb8..28020f98c7 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -322,7 +322,7 @@ App::post('/v1/teams/:teamId/memberships') ->inject('events') ->action(function (string $teamId, string $userId, string $email, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $mails, EventPhone $messaging, Event $events) { - if(empty($userId) && empty($email) && empty($phone)) { + if (empty($userId) && empty($email) && empty($phone)) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'At least one of userId, email, or phone is required'); } $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); @@ -340,25 +340,25 @@ App::post('/v1/teams/:teamId/memberships') throw new Exception(Exception::TEAM_NOT_FOUND); } - if(!empty($userId)) { + if (!empty($userId)) { $invitee = $dbForProject->getDocument('users', $userId); - if($invitee->isEmpty()) { + if ($invitee->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND, 'User with given userId doesn\'t exist.', 404); } - if(!empty($email) && $invitee->getAttribute('email', '') != $email) { + if (!empty($email) && $invitee->getAttribute('email', '') != $email) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given userId and email doesn\'t match', 409); } - if(!empty($phone) && $invitee->getAttribute('phone', '') != $phone) { + if (!empty($phone) && $invitee->getAttribute('phone', '') != $phone) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given userId and phone doesn\'t match', 409); } - } else if(!empty($email)) { + } elseif (!empty($email)) { $invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address - if(!$invitee->isEmpty() && !empty($phone) && $invitee->getAttribute('phone', '') != $phone) { + if (!$invitee->isEmpty() && !empty($phone) && $invitee->getAttribute('phone', '') != $phone) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given email and phone doesn\'t match', 409); } - }else if(!empty($phone)) { + } elseif (!empty($phone)) { $invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]); - if(!$invitee->isEmpty() && !empty($email) && $invitee->getAttribute('email', '') != $email) { + if (!$invitee->isEmpty() && !empty($email) && $invitee->getAttribute('email', '') != $email) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409); } } @@ -464,7 +464,7 @@ App::post('/v1/teams/:teamId/memberships') $url = Template::unParseURL($url); if (!$isPrivilegedUser && !$isAppUser) { // No need of confirmation when in admin or app mode - if(!empty($email)) { + if (!empty($email)) { $mails ->setType(MAIL_TYPE_INVITATION) ->setRecipient($email) @@ -477,7 +477,7 @@ App::post('/v1/teams/:teamId/memberships') ; } - if(@empty($phone)) { + if (@empty($phone)) { $messaging ->setRecipient($phone) ->setMessage($url) From 63a981591dce25e74cc6403502f4c113f659f671 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 9 Jan 2023 08:25:15 +0000 Subject: [PATCH 03/18] fix error and param order --- app/controllers/api/teams.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 28020f98c7..05ce3bd1b9 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -306,8 +306,8 @@ App::post('/v1/teams/:teamId/memberships') ->label('sdk.response.model', Response::MODEL_MEMBERSHIP) ->label('abuse-limit', 10) ->param('teamId', '', new UID(), 'Team ID.') - ->param('userId', '', new UID(), 'User ID.', true) ->param('email', '', new Email(), 'Email of the new team member.', true) + ->param('userId', '', new UID(), 'User ID.', true) ->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) ->param('roles', [], new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.') ->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) // TODO add our own built-in confirm page @@ -477,7 +477,7 @@ App::post('/v1/teams/:teamId/memberships') ; } - if (@empty($phone)) { + if (!empty($phone)) { $messaging ->setRecipient($phone) ->setMessage($url) From 29ef377b60bcf5acf9070c6c2d63960879177e47 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 9 Jan 2023 08:29:33 +0000 Subject: [PATCH 04/18] refactor privileged/app user check --- app/controllers/api/teams.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 05ce3bd1b9..00be7ac896 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -457,13 +457,11 @@ App::post('/v1/teams/:teamId/memberships') } catch (Duplicate $th) { throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS); } - } - - $url = Template::parseURL($url); - $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['membershipId' => $membership->getId(), 'userId' => $invitee->getId(), 'secret' => $secret, 'teamId' => $teamId]); - $url = Template::unParseURL($url); - - if (!$isPrivilegedUser && !$isAppUser) { // No need of confirmation when in admin or app mode + $url = Template::parseURL($url); + $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['membershipId' => $membership->getId(), 'userId' => $invitee->getId(), 'secret' => $secret, 'teamId' => $teamId]); + $url = Template::unParseURL($url); + + // No need of confirmation when in admin or app mode if (!empty($email)) { $mails ->setType(MAIL_TYPE_INVITATION) From 32b8a1edfebfc46ec9d26c33914bd6d6d9c9d394 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 9 Jan 2023 08:44:02 +0000 Subject: [PATCH 05/18] fix param order in action --- app/controllers/api/teams.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 00be7ac896..37ff807d4c 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -320,7 +320,7 @@ App::post('/v1/teams/:teamId/memberships') ->inject('mails') ->inject('messaging') ->inject('events') - ->action(function (string $teamId, string $userId, string $email, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $mails, EventPhone $messaging, Event $events) { + ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $mails, EventPhone $messaging, Event $events) { if (empty($userId) && empty($email) && empty($phone)) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'At least one of userId, email, or phone is required'); From bc7b98821af5bd9c8e93cbefa2c71f9b32bae58b Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 9 Jan 2023 08:56:18 +0000 Subject: [PATCH 06/18] fix error --- app/controllers/api/teams.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 37ff807d4c..1c1d63db18 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -339,7 +339,6 @@ App::post('/v1/teams/:teamId/memberships') if ($team->isEmpty()) { throw new Exception(Exception::TEAM_NOT_FOUND); } - if (!empty($userId)) { $invitee = $dbForProject->getDocument('users', $userId); if ($invitee->isEmpty()) { @@ -353,12 +352,12 @@ App::post('/v1/teams/:teamId/memberships') } } elseif (!empty($email)) { $invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address - if (!$invitee->isEmpty() && !empty($phone) && $invitee->getAttribute('phone', '') != $phone) { + if (!empty($invitee) && !empty($phone) && $invitee->getAttribute('phone', '') != $phone) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given email and phone doesn\'t match', 409); } } elseif (!empty($phone)) { $invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]); - if (!$invitee->isEmpty() && !empty($email) && $invitee->getAttribute('email', '') != $email) { + if (!empty($invitee) && !empty($email) && $invitee->getAttribute('email', '') != $email) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409); } } From 8f351ad7b2d5e3b1bbd459fc29d4b35726652a83 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 9 Jan 2023 09:17:39 +0000 Subject: [PATCH 07/18] set email and phone attribute once user found --- app/controllers/api/teams.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 1c1d63db18..170609f0ef 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -350,6 +350,9 @@ App::post('/v1/teams/:teamId/memberships') if (!empty($phone) && $invitee->getAttribute('phone', '') != $phone) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given userId and phone doesn\'t match', 409); } + $email = $invitee->getAttribute('email', ''); + $phone = $invitee->getAttribute('phone', ''); + $name = empty($name) ? $invitee->getAttribute('name', '') : $name; } elseif (!empty($email)) { $invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address if (!empty($invitee) && !empty($phone) && $invitee->getAttribute('phone', '') != $phone) { From 5088c9ea99b8c35ac0aa6c8628ebcf3252549a83 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 9 Jan 2023 09:17:45 +0000 Subject: [PATCH 08/18] simple test --- tests/e2e/Services/Teams/TeamsBaseClient.php | 71 ++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/tests/e2e/Services/Teams/TeamsBaseClient.php b/tests/e2e/Services/Teams/TeamsBaseClient.php index 788626b102..e0f4dbf657 100644 --- a/tests/e2e/Services/Teams/TeamsBaseClient.php +++ b/tests/e2e/Services/Teams/TeamsBaseClient.php @@ -216,6 +216,69 @@ trait TeamsBaseClient $membershipUid = substr($lastEmail['text'], strpos($lastEmail['text'], '?membershipId=', 0) + 14, 20); $userUid = substr($lastEmail['text'], strpos($lastEmail['text'], '&userId=', 0) + 8, 20); + /** + * Test with UserId + * Create user + */ + $secondEmail = uniqid() . 'foe@localhost.test'; + $secondName = 'Another Foe'; + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'userId' => 'unique()', + 'email' => $secondEmail, + 'password' => 'password', + 'name' => $secondName + ]); + $this->assertEquals(201, $response['headers']['status-code']); + $userId = $response['body']['$id']; + + /** + * Test for UserID + * Failure + */ + $response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'userId' => 'abcdefdg', + 'roles' => ['admin', 'editor'], + 'url' => 'http://localhost:5000/join-us#title' + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for UserID + * SUCCESS + */ + $response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'userId' => $userId, + 'roles' => ['admin', 'editor'], + 'url' => 'http://localhost:5000/join-us#title' + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertNotEmpty($response['body']['userId']); + $this->assertEquals($secondName, $response['body']['userName']); + $this->assertEquals($secondEmail, $response['body']['userEmail']); + $this->assertNotEmpty($response['body']['teamId']); + $this->assertNotEmpty($response['body']['teamName']); + $this->assertCount(2, $response['body']['roles']); + $this->assertEquals(false, DateTime::isValid($response['body']['joined'])); // is null in DB + $this->assertEquals(false, $response['body']['confirm']); + + $lastEmail = $this->getLastEmail(); + + $this->assertEquals($secondEmail, $lastEmail['to'][0]['address']); + $this->assertEquals($secondName, $lastEmail['to'][0]['name']); + $this->assertEquals('Invitation to ' . $teamName . ' Team at ' . $this->getProject()['name'], $lastEmail['subject']); + /** * Test for FAILURE */ @@ -292,7 +355,7 @@ trait TeamsBaseClient $this->assertEquals(200, $memberships['headers']['status-code']); $this->assertIsInt($memberships['body']['total']); $this->assertNotEmpty($memberships['body']['memberships']); - $this->assertCount(2, $memberships['body']['memberships']); + $this->assertCount(3, $memberships['body']['memberships']); $response = $this->client->call(Client::METHOD_GET, '/teams/' . $data['teamUid'] . '/memberships', array_merge([ 'content-type' => 'application/json', @@ -304,7 +367,7 @@ trait TeamsBaseClient $this->assertEquals(200, $response['headers']['status-code']); $this->assertIsInt($response['body']['total']); $this->assertNotEmpty($response['body']['memberships']); - $this->assertCount(1, $response['body']['memberships']); + $this->assertCount(2, $response['body']['memberships']); $this->assertEquals($memberships['body']['memberships'][1]['$id'], $response['body']['memberships'][0]['$id']); } @@ -560,7 +623,7 @@ trait TeamsBaseClient ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(2, $response['body']['total']); + $this->assertEquals(3, $response['body']['total']); $ownerMembershipUid = $response['body']['memberships'][0]['$id']; @@ -615,7 +678,7 @@ trait TeamsBaseClient ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(1, $response['body']['total']); + $this->assertEquals(2, $response['body']['total']); /** * Test for when the owner tries to delete their membership From 34b22802076eaa26c5947a46cff1850cad4b65ea Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 9 Jan 2023 09:18:13 +0000 Subject: [PATCH 09/18] fix format --- app/controllers/api/teams.php | 2 +- tests/e2e/Services/Teams/TeamsBaseClient.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 170609f0ef..7731d4f946 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -462,7 +462,7 @@ App::post('/v1/teams/:teamId/memberships') $url = Template::parseURL($url); $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['membershipId' => $membership->getId(), 'userId' => $invitee->getId(), 'secret' => $secret, 'teamId' => $teamId]); $url = Template::unParseURL($url); - + // No need of confirmation when in admin or app mode if (!empty($email)) { $mails diff --git a/tests/e2e/Services/Teams/TeamsBaseClient.php b/tests/e2e/Services/Teams/TeamsBaseClient.php index e0f4dbf657..e6ad406098 100644 --- a/tests/e2e/Services/Teams/TeamsBaseClient.php +++ b/tests/e2e/Services/Teams/TeamsBaseClient.php @@ -278,7 +278,7 @@ trait TeamsBaseClient $this->assertEquals($secondEmail, $lastEmail['to'][0]['address']); $this->assertEquals($secondName, $lastEmail['to'][0]['name']); $this->assertEquals('Invitation to ' . $teamName . ' Team at ' . $this->getProject()['name'], $lastEmail['subject']); - + /** * Test for FAILURE */ From a2642ecc67e54cd2fa21e3a3f719534c01dbd34c Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 9 Jan 2023 10:59:28 +0000 Subject: [PATCH 10/18] send phone invite only if email is empty --- app/controllers/api/teams.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 7731d4f946..0a95465cc4 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -475,9 +475,7 @@ App::post('/v1/teams/:teamId/memberships') ->setUser($user) ->trigger() ; - } - - if (!empty($phone)) { + } else if (!empty($phone)) { $messaging ->setRecipient($phone) ->setMessage($url) From 11b3739360c2662c75057a801a1ccca1d440043d Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 9 Jan 2023 11:10:02 +0000 Subject: [PATCH 11/18] fix when either email or phone were empty --- app/controllers/api/teams.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 0a95465cc4..6c0bb4875e 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -354,11 +354,14 @@ App::post('/v1/teams/:teamId/memberships') $phone = $invitee->getAttribute('phone', ''); $name = empty($name) ? $invitee->getAttribute('name', '') : $name; } elseif (!empty($email)) { + var_dump("email not empty"); $invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address + var_dump($invitee); if (!empty($invitee) && !empty($phone) && $invitee->getAttribute('phone', '') != $phone) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given email and phone doesn\'t match', 409); } } elseif (!empty($phone)) { + var_dump("phone not empty"); $invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]); if (!empty($invitee) && !empty($email) && $invitee->getAttribute('email', '') != $email) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409); @@ -386,8 +389,8 @@ App::post('/v1/teams/:teamId/memberships') Permission::update(Role::user($userId)), Permission::delete(Role::user($userId)), ], - 'email' => $email, - 'phone' => $phone, + 'email' => empty($email) ? null : $email, + 'phone' => empty($phone) ? null : $phone, 'emailVerification' => false, 'status' => true, 'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS), @@ -409,6 +412,7 @@ App::post('/v1/teams/:teamId/memberships') 'search' => implode(' ', [$userId, $email, $name]) ]))); } catch (Duplicate $th) { + var_dump($th->getMessage()); throw new Exception(Exception::USER_ALREADY_EXISTS); } } From 8d40220114851c62a3ce2a629840350717671fcc Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 9 Jan 2023 11:10:50 +0000 Subject: [PATCH 12/18] remove var dumps --- app/controllers/api/teams.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 6c0bb4875e..4c6b2496db 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -354,14 +354,11 @@ App::post('/v1/teams/:teamId/memberships') $phone = $invitee->getAttribute('phone', ''); $name = empty($name) ? $invitee->getAttribute('name', '') : $name; } elseif (!empty($email)) { - var_dump("email not empty"); $invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address - var_dump($invitee); if (!empty($invitee) && !empty($phone) && $invitee->getAttribute('phone', '') != $phone) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given email and phone doesn\'t match', 409); } } elseif (!empty($phone)) { - var_dump("phone not empty"); $invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]); if (!empty($invitee) && !empty($email) && $invitee->getAttribute('email', '') != $email) { throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409); @@ -412,7 +409,6 @@ App::post('/v1/teams/:teamId/memberships') 'search' => implode(' ', [$userId, $email, $name]) ]))); } catch (Duplicate $th) { - var_dump($th->getMessage()); throw new Exception(Exception::USER_ALREADY_EXISTS); } } From b06ce912972bbb6fc7fc1766213bf804731e7a15 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 9 Jan 2023 11:11:55 +0000 Subject: [PATCH 13/18] fix formatting --- app/controllers/api/teams.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 4c6b2496db..26f8499998 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -475,7 +475,7 @@ App::post('/v1/teams/:teamId/memberships') ->setUser($user) ->trigger() ; - } else if (!empty($phone)) { + } elseif (!empty($phone)) { $messaging ->setRecipient($phone) ->setMessage($url) From afaa64b54bac6151d00d33fe64f19a9708d65a60 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Fri, 13 Jan 2023 06:05:16 +0000 Subject: [PATCH 14/18] fix invitation after merge --- app/controllers/api/teams.php | 68 +++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index d84be2181e..b9a5d4096b 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -459,44 +459,50 @@ App::post('/v1/teams/:teamId/memberships') } catch (Duplicate $th) { throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS); } - + $url = Template::parseURL($url); $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['membershipId' => $membership->getId(), 'userId' => $invitee->getId(), 'secret' => $secret, 'teamId' => $teamId]); $url = Template::unParseURL($url); + if (!empty($email)) { + $projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]'); - $projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]'); + $from = $project->isEmpty() || $project->getId() === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $projectName); + $body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl'); + $subject = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName); + $body->setParam('{{owner}}', $user->getAttribute('name')); + $body->setParam('{{team}}', $team->getAttribute('name')); - $from = $project->isEmpty() || $project->getId() === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $projectName); - $body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl'); - $subject = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName); - $body->setParam('{{owner}}', $user->getAttribute('name')); - $body->setParam('{{team}}', $team->getAttribute('name')); + $body + ->setParam('{{subject}}', $subject) + ->setParam('{{hello}}', $locale->getText("emails.invitation.hello")) + ->setParam('{{name}}', $user->getAttribute('name')) + ->setParam('{{body}}', $locale->getText("emails.invitation.body")) + ->setParam('{{redirect}}', $url) + ->setParam('{{footer}}', $locale->getText("emails.invitation.footer")) + ->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks")) + ->setParam('{{signature}}', $locale->getText("emails.invitation.signature")) + ->setParam('{{project}}', $projectName) + ->setParam('{{direction}}', $locale->getText('settings.direction')) + ->setParam('{{bg-body}}', '#f7f7f7') + ->setParam('{{bg-content}}', '#ffffff') + ->setParam('{{text-content}}', '#000000'); - $body - ->setParam('{{subject}}', $subject) - ->setParam('{{hello}}', $locale->getText("emails.invitation.hello")) - ->setParam('{{name}}', $user->getAttribute('name')) - ->setParam('{{body}}', $locale->getText("emails.invitation.body")) - ->setParam('{{redirect}}', $url) - ->setParam('{{footer}}', $locale->getText("emails.invitation.footer")) - ->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks")) - ->setParam('{{signature}}', $locale->getText("emails.invitation.signature")) - ->setParam('{{project}}', $projectName) - ->setParam('{{direction}}', $locale->getText('settings.direction')) - ->setParam('{{bg-body}}', '#f7f7f7') - ->setParam('{{bg-content}}', '#ffffff') - ->setParam('{{text-content}}', '#000000'); + $body = $body->render(); - $body = $body->render(); - - $mails - ->setSubject($subject) - ->setBody($body) - ->setFrom($from) - ->setRecipient($invitee->getAttribute('email')) - ->setName($invitee->getAttribute('name')) - ->trigger() - ; + $mails + ->setSubject($subject) + ->setBody($body) + ->setFrom($from) + ->setRecipient($invitee->getAttribute('email')) + ->setName($invitee->getAttribute('name')) + ->trigger() + ; + } elseif (!empty($phone)) { + $messaging + ->setRecipient($phone) + ->setMessage($url) + ->trigger(); + } } $events From 2c23f7f1497c0071ea673a4b6c65a42f35ad3138 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 17 Jan 2023 07:17:08 +0545 Subject: [PATCH 15/18] update doc --- docs/references/teams/create-team-membership.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/references/teams/create-team-membership.md b/docs/references/teams/create-team-membership.md index 7668c69b99..c364f4fdde 100644 --- a/docs/references/teams/create-team-membership.md +++ b/docs/references/teams/create-team-membership.md @@ -1,5 +1,7 @@ -Invite a new member to join your team. If initiated from the client SDK, an email with a link to join the team will be sent to the member's email address and an account will be created for them should they not be signed up already. If initiated from server-side SDKs, the new member will automatically be added to the team. +Invite a new member to join your team. You can also add existing users with their unique ID. You can also invite using a phone number. If initiated from the client SDK, an email or sms with a link to join the team will be sent to the member's email address or phone number and an account will be created for them should they not exist already. If initiated from server-side SDKs, the new member will automatically be added to the team. + +You can only provide one of the User ID, email or phone number to add member to the team. And the priority will be User ID > email and > phone number. Use the 'url' parameter to redirect the user from the invitation email back to your app. When the user is redirected, use the [Update Team Membership Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow the user to accept the invitation to the team. -Please note that to avoid a [Redirect Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URL's are the once from domains you have set when adding your platforms in the console interface. \ No newline at end of file +Please note that to avoid a [Redirect Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URL's are the once from domains you have set when adding your platforms in the console interface. From 60bfae7e004c83d235628657de132922489e3b02 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 17 Jan 2023 10:14:32 -0500 Subject: [PATCH 16/18] Update create-team-membership.md --- docs/references/teams/create-team-membership.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/references/teams/create-team-membership.md b/docs/references/teams/create-team-membership.md index c364f4fdde..d7272c7a47 100644 --- a/docs/references/teams/create-team-membership.md +++ b/docs/references/teams/create-team-membership.md @@ -1,7 +1,7 @@ -Invite a new member to join your team. You can also add existing users with their unique ID. You can also invite using a phone number. If initiated from the client SDK, an email or sms with a link to join the team will be sent to the member's email address or phone number and an account will be created for them should they not exist already. If initiated from server-side SDKs, the new member will automatically be added to the team. +Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, an email or sms with a link to join the team will be sent to the invited user and an account will be created for them if one doesn't exist. If initiated from a Server SDKs, the new member will be added automatically to the team. -You can only provide one of the User ID, email or phone number to add member to the team. And the priority will be User ID > email and > phone number. +You only need to provide one of User ID, email, or phone number. When multiple are provided, the priority will be user ID > email > phone number, lower priority parameters are ignored. Use the 'url' parameter to redirect the user from the invitation email back to your app. When the user is redirected, use the [Update Team Membership Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow the user to accept the invitation to the team. -Please note that to avoid a [Redirect Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URL's are the once from domains you have set when adding your platforms in the console interface. +Please note that to avoid a [Redirect Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only redirect URLs under the domains you have added as a platform on the Appwrite Console will be accepted. From 33dab2ea46768fa6460bedcc1b77ede0d45a3222 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 17 Jan 2023 10:18:50 -0500 Subject: [PATCH 17/18] Update create-team-membership.md --- docs/references/teams/create-team-membership.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/references/teams/create-team-membership.md b/docs/references/teams/create-team-membership.md index d7272c7a47..ffa52b83c2 100644 --- a/docs/references/teams/create-team-membership.md +++ b/docs/references/teams/create-team-membership.md @@ -1,7 +1,7 @@ -Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, an email or sms with a link to join the team will be sent to the invited user and an account will be created for them if one doesn't exist. If initiated from a Server SDKs, the new member will be added automatically to the team. +Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team. -You only need to provide one of User ID, email, or phone number. When multiple are provided, the priority will be user ID > email > phone number, lower priority parameters are ignored. +You only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters. -Use the 'url' parameter to redirect the user from the invitation email back to your app. When the user is redirected, use the [Update Team Membership Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow the user to accept the invitation to the team. +Use the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow the user to accept the invitation to the team. -Please note that to avoid a [Redirect Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only redirect URLs under the domains you have added as a platform on the Appwrite Console will be accepted. +Please note that to avoid a [Redirect Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console. From 1466e7b7702ccc7dbc163ebb0606d6c92cc799d9 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 18 Jan 2023 09:33:47 +0545 Subject: [PATCH 18/18] Update app/controllers/api/teams.php Co-authored-by: Vincent (Wen Yu) Ge --- app/controllers/api/teams.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index b9a5d4096b..a08085f692 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -307,7 +307,7 @@ App::post('/v1/teams/:teamId/memberships') ->label('abuse-limit', 10) ->param('teamId', '', new UID(), 'Team ID.') ->param('email', '', new Email(), 'Email of the new team member.', true) - ->param('userId', '', new UID(), 'User ID.', true) + ->param('userId', '', new UID(), 'ID of the user to be added to a team.', true) ->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) ->param('roles', [], new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.') ->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) // TODO add our own built-in confirm page