From b5449a8ca6f547054972af93178a7c41e497dd37 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 13 Jan 2025 18:25:48 +0000 Subject: [PATCH 01/12] fix: resend invitation --- app/controllers/api/teams.php | 62 ++++++++++++-------- tests/e2e/Services/Teams/TeamsBaseClient.php | 10 +++- tests/e2e/Services/Teams/TeamsBaseServer.php | 10 +++- 3 files changed, 52 insertions(+), 30 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 461e90da58..0ab166e28d 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -546,33 +546,45 @@ App::post('/v1/teams/:teamId/memberships') throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to send invitations for this team'); } - $secret = Auth::tokenGenerator(); - - $membershipId = ID::unique(); - $membership = new Document([ - '$id' => $membershipId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::user($invitee->getId())), - Permission::update(Role::team($team->getId(), 'owner')), - Permission::delete(Role::user($invitee->getId())), - Permission::delete(Role::team($team->getId(), 'owner')), - ], - 'userId' => $invitee->getId(), - 'userInternalId' => $invitee->getInternalId(), - 'teamId' => $team->getId(), - 'teamInternalId' => $team->getInternalId(), - 'roles' => $roles, - 'invited' => DateTime::now(), - 'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null, - 'confirm' => ($isPrivilegedUser || $isAppUser), - 'secret' => Auth::hash($secret), - 'search' => implode(' ', [$membershipId, $invitee->getId()]) + $membership = $dbForProject->findOne('memberships', [ + Query::equal('userId', [$invitee->getId()]), + Query::equal('teamId', [$team->getId()]), ]); + $createdMembership = false; + + if ($membership->isEmpty()) { + $secret = Auth::tokenGenerator(); + + $membershipId = ID::unique(); + $membership = new Document([ + '$id' => $membershipId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::user($invitee->getId())), + Permission::update(Role::team($team->getId(), 'owner')), + Permission::delete(Role::user($invitee->getId())), + Permission::delete(Role::team($team->getId(), 'owner')), + ], + 'userId' => $invitee->getId(), + 'userInternalId' => $invitee->getInternalId(), + 'teamId' => $team->getId(), + 'teamInternalId' => $team->getInternalId(), + 'roles' => $roles, + 'invited' => DateTime::now(), + 'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null, + 'confirm' => ($isPrivilegedUser || $isAppUser), + 'secret' => Auth::hash($secret), + 'search' => implode(' ', [$membershipId, $invitee->getId()]) + ]); + + $createdMembership = true; + } if ($isPrivilegedUser || $isAppUser) { // Allow admin to create membership try { - $membership = Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)); + $membership = $createdMembership ? + Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) : + Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)); } catch (Duplicate $th) { throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS); } @@ -582,7 +594,9 @@ App::post('/v1/teams/:teamId/memberships') $dbForProject->purgeCachedDocument('users', $invitee->getId()); } else { try { - $membership = $dbForProject->createDocument('memberships', $membership); + $membership = $createdMembership ? + $dbForProject->createDocument('memberships', $membership) : + $dbForProject->updateDocument('memberships', $membership->getId(), $membership); } catch (Duplicate $th) { throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS); } diff --git a/tests/e2e/Services/Teams/TeamsBaseClient.php b/tests/e2e/Services/Teams/TeamsBaseClient.php index ca6082f6d0..cf011d2bf7 100644 --- a/tests/e2e/Services/Teams/TeamsBaseClient.php +++ b/tests/e2e/Services/Teams/TeamsBaseClient.php @@ -292,9 +292,9 @@ trait TeamsBaseClient $this->assertEquals('Invitation to ' . $teamName . ' Team at ' . $this->getProject()['name'], $lastEmail['subject']); /** - * Test for FAILURE + * Test for resend + * SUCCESS */ - $response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -305,7 +305,11 @@ trait TeamsBaseClient 'url' => 'http://localhost:5000/join-us#title' ]); - $this->assertEquals(409, $response['headers']['status-code']); + $this->assertEquals(201, $response['headers']['status-code']); + + /** + * Test for FAILURE + */ $response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([ 'content-type' => 'application/json', diff --git a/tests/e2e/Services/Teams/TeamsBaseServer.php b/tests/e2e/Services/Teams/TeamsBaseServer.php index 6a1d05e9d4..226536c604 100644 --- a/tests/e2e/Services/Teams/TeamsBaseServer.php +++ b/tests/e2e/Services/Teams/TeamsBaseServer.php @@ -186,9 +186,9 @@ trait TeamsBaseServer // $this->assertContains('team:'.$teamUid.'/editor', $response['body']['roles']); /** - * Test for FAILURE + * Test for resend + * SUCCESS */ - $response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -199,7 +199,11 @@ trait TeamsBaseServer 'url' => 'http://localhost:5000/join-us#title' ]); - $this->assertEquals(409, $response['headers']['status-code']); + $this->assertEquals(201, $response['headers']['status-code']); + + /** + * Test for FAILURE + */ $response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([ 'content-type' => 'application/json', From e6cb17a6cc99ad73bcc65c6e18aad944d66b6b35 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 13 Jan 2025 18:51:23 +0000 Subject: [PATCH 02/12] chore: remove TEAM_INVITE_ALREADY_EXISTS --- app/config/errors.php | 5 ----- app/controllers/api/teams.php | 26 ++++++++++++-------------- src/Appwrite/Extend/Exception.php | 1 - 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/app/config/errors.php b/app/config/errors.php index f09d1596eb..1c32accdb4 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -349,11 +349,6 @@ return [ 'description' => 'Team with the requested ID could not be found.', 'code' => 404, ], - Exception::TEAM_INVITE_ALREADY_EXISTS => [ - 'name' => Exception::TEAM_INVITE_ALREADY_EXISTS, - 'description' => 'User has already been invited or is already a member of this team', - 'code' => 409, - ], Exception::TEAM_INVITE_NOT_FOUND => [ 'name' => Exception::TEAM_INVITE_NOT_FOUND, 'description' => 'The requested team invitation could not be found.', diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 0ab166e28d..829d452cae 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -581,24 +581,22 @@ App::post('/v1/teams/:teamId/memberships') } if ($isPrivilegedUser || $isAppUser) { // Allow admin to create membership - try { - $membership = $createdMembership ? - Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) : - Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)); - } catch (Duplicate $th) { - throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS); - } + $membership = $createdMembership ? + Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) : + Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)); - Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); + if ($createdMembership) { + Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); + } $dbForProject->purgeCachedDocument('users', $invitee->getId()); } else { - try { - $membership = $createdMembership ? - $dbForProject->createDocument('memberships', $membership) : - $dbForProject->updateDocument('memberships', $membership->getId(), $membership); - } catch (Duplicate $th) { - throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS); + $membership = $createdMembership ? + $dbForProject->createDocument('memberships', $membership) : + $dbForProject->updateDocument('memberships', $membership->getId(), $membership); + + if ($createdMembership) { + $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1); } $url = Template::parseURL($url); diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 54bf6d96ea..4a6959f332 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -112,7 +112,6 @@ class Exception extends \Exception /** Teams */ public const TEAM_NOT_FOUND = 'team_not_found'; - public const TEAM_INVITE_ALREADY_EXISTS = 'team_invite_already_exists'; public const TEAM_INVITE_NOT_FOUND = 'team_invite_not_found'; public const TEAM_INVALID_SECRET = 'team_invalid_secret'; public const TEAM_MEMBERSHIP_MISMATCH = 'team_membership_mismatch'; From 2571f7d3fc37beea50c62efdb774aef437bde0e0 Mon Sep 17 00:00:00 2001 From: ChiragAgg5k Date: Tue, 14 Jan 2025 15:43:00 +0530 Subject: [PATCH 03/12] refactor: createdMembership --- app/controllers/api/teams.php | 266 ++++++++++++++++------------------ 1 file changed, 127 insertions(+), 139 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 829d452cae..3664afec43 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -550,7 +550,6 @@ App::post('/v1/teams/:teamId/memberships') Query::equal('userId', [$invitee->getId()]), Query::equal('teamId', [$team->getId()]), ]); - $createdMembership = false; if ($membership->isEmpty()) { $secret = Auth::tokenGenerator(); @@ -577,159 +576,148 @@ App::post('/v1/teams/:teamId/memberships') 'search' => implode(' ', [$membershipId, $invitee->getId()]) ]); - $createdMembership = true; + $membership = ($isPrivilegedUser || $isAppUser) ? + Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) : + $dbForProject->createDocument('memberships', $membership); + } else { + + $membership = ($isPrivilegedUser || $isAppUser) ? + Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) : + $dbForProject->updateDocument('memberships', $membership->getId(), $membership); } - if ($isPrivilegedUser || $isAppUser) { // Allow admin to create membership - $membership = $createdMembership ? - Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) : - Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)); + $dbForProject->purgeCachedDocument('users', $invitee->getId()); - if ($createdMembership) { - Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); - } + $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]'); - $dbForProject->purgeCachedDocument('users', $invitee->getId()); - } else { - $membership = $createdMembership ? - $dbForProject->createDocument('memberships', $membership) : - $dbForProject->updateDocument('memberships', $membership->getId(), $membership); + $body = $locale->getText("emails.invitation.body"); + $subject = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName); + $customTemplate = $project->getAttribute('templates', [])['email.invitation-' . $locale->default] ?? []; - if ($createdMembership) { - $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1); - } + $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); + $message + ->setParam('{{body}}', $body, escapeHtml: false) + ->setParam('{{hello}}', $locale->getText("emails.invitation.hello")) + ->setParam('{{footer}}', $locale->getText("emails.invitation.footer")) + ->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks")) + ->setParam('{{signature}}', $locale->getText("emails.invitation.signature")); + $body = $message->render(); - $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]'); + $smtp = $project->getAttribute('smtp', []); + $smtpEnabled = $smtp['enabled'] ?? false; - $body = $locale->getText("emails.invitation.body"); - $subject = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName); - $customTemplate = $project->getAttribute('templates', [])['email.invitation-' . $locale->default] ?? []; + $senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); + $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); + $replyTo = ""; - $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); - $message - ->setParam('{{body}}', $body, escapeHtml: false) - ->setParam('{{hello}}', $locale->getText("emails.invitation.hello")) - ->setParam('{{footer}}', $locale->getText("emails.invitation.footer")) - ->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks")) - ->setParam('{{signature}}', $locale->getText("emails.invitation.signature")); - $body = $message->render(); - - $smtp = $project->getAttribute('smtp', []); - $smtpEnabled = $smtp['enabled'] ?? false; - - $senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); - $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); - $replyTo = ""; - - if ($smtpEnabled) { - if (!empty($smtp['senderEmail'])) { - $senderEmail = $smtp['senderEmail']; - } - if (!empty($smtp['senderName'])) { - $senderName = $smtp['senderName']; - } - if (!empty($smtp['replyTo'])) { - $replyTo = $smtp['replyTo']; - } - - $queueForMails - ->setSmtpHost($smtp['host'] ?? '') - ->setSmtpPort($smtp['port'] ?? '') - ->setSmtpUsername($smtp['username'] ?? '') - ->setSmtpPassword($smtp['password'] ?? '') - ->setSmtpSecure($smtp['secure'] ?? ''); - - if (!empty($customTemplate)) { - if (!empty($customTemplate['senderEmail'])) { - $senderEmail = $customTemplate['senderEmail']; - } - if (!empty($customTemplate['senderName'])) { - $senderName = $customTemplate['senderName']; - } - if (!empty($customTemplate['replyTo'])) { - $replyTo = $customTemplate['replyTo']; - } - - $body = $customTemplate['message'] ?? ''; - $subject = $customTemplate['subject'] ?? $subject; - } - - $queueForMails - ->setSmtpReplyTo($replyTo) - ->setSmtpSenderEmail($senderEmail) - ->setSmtpSenderName($senderName); + if ($smtpEnabled) { + if (!empty($smtp['senderEmail'])) { + $senderEmail = $smtp['senderEmail']; + } + if (!empty($smtp['senderName'])) { + $senderName = $smtp['senderName']; + } + if (!empty($smtp['replyTo'])) { + $replyTo = $smtp['replyTo']; } - - $emailVariables = [ - 'owner' => $user->getAttribute('name'), - 'direction' => $locale->getText('settings.direction'), - /* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */ - 'user' => $user->getAttribute('name'), - 'team' => $team->getAttribute('name'), - 'redirect' => $url, - 'project' => $projectName - ]; $queueForMails - ->setSubject($subject) - ->setBody($body) - ->setRecipient($invitee->getAttribute('email')) - ->setName($invitee->getAttribute('name')) - ->setVariables($emailVariables) - ->trigger() - ; - } elseif (!empty($phone)) { - if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { - throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); - } + ->setSmtpHost($smtp['host'] ?? '') + ->setSmtpPort($smtp['port'] ?? '') + ->setSmtpUsername($smtp['username'] ?? '') + ->setSmtpPassword($smtp['password'] ?? '') + ->setSmtpSecure($smtp['secure'] ?? ''); - $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl'); - - $customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? []; if (!empty($customTemplate)) { - $message = $customTemplate['message']; - } - - $message = $message->setParam('{{token}}', $url); - $message = $message->render(); - - $messageDoc = new Document([ - '$id' => ID::unique(), - 'data' => [ - 'content' => $message, - ], - ]); - - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_INTERNAL) - ->setMessage($messageDoc) - ->setRecipients([$phone]) - ->setProviderType('SMS'); - - if (isset($plan['authPhone'])) { - $timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days - $timelimit - ->setParam('{organizationId}', $project->getAttribute('teamId')); - - $abuse = new Abuse($timelimit); - if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') { - $helper = PhoneNumberUtil::getInstance(); - $countryCode = $helper->parse($phone)->getCountryCode(); - - if (!empty($countryCode)) { - $queueForUsage - ->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1); - } + if (!empty($customTemplate['senderEmail'])) { + $senderEmail = $customTemplate['senderEmail']; } - $queueForUsage - ->addMetric(METRIC_AUTH_METHOD_PHONE, 1) - ->setProject($project) - ->trigger(); + if (!empty($customTemplate['senderName'])) { + $senderName = $customTemplate['senderName']; + } + if (!empty($customTemplate['replyTo'])) { + $replyTo = $customTemplate['replyTo']; + } + + $body = $customTemplate['message'] ?? ''; + $subject = $customTemplate['subject'] ?? $subject; } + + $queueForMails + ->setSmtpReplyTo($replyTo) + ->setSmtpSenderEmail($senderEmail) + ->setSmtpSenderName($senderName); + } + + $emailVariables = [ + 'owner' => $user->getAttribute('name'), + 'direction' => $locale->getText('settings.direction'), + /* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */ + 'user' => $user->getAttribute('name'), + 'team' => $team->getAttribute('name'), + 'redirect' => $url, + 'project' => $projectName + ]; + + $queueForMails + ->setSubject($subject) + ->setBody($body) + ->setRecipient($invitee->getAttribute('email')) + ->setName($invitee->getAttribute('name')) + ->setVariables($emailVariables) + ->trigger(); + + } elseif (!empty($phone)) { + if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { + throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); + } + + $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl'); + + $customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? []; + if (!empty($customTemplate)) { + $message = $customTemplate['message']; + } + + $message = $message->setParam('{{token}}', $url); + $message = $message->render(); + + $messageDoc = new Document([ + '$id' => ID::unique(), + 'data' => [ + 'content' => $message, + ], + ]); + + $queueForMessaging + ->setType(MESSAGE_SEND_TYPE_INTERNAL) + ->setMessage($messageDoc) + ->setRecipients([$phone]) + ->setProviderType('SMS'); + + if (isset($plan['authPhone'])) { + $timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days + $timelimit + ->setParam('{organizationId}', $project->getAttribute('teamId')); + + $abuse = new Abuse($timelimit); + if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') { + $helper = PhoneNumberUtil::getInstance(); + $countryCode = $helper->parse($phone)->getCountryCode(); + + if (!empty($countryCode)) { + $queueForUsage + ->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1); + } + } + $queueForUsage + ->addMetric(METRIC_AUTH_METHOD_PHONE, 1) + ->setProject($project) + ->trigger(); } } From 1c7867ee039f1cb3600313614616c026cf05eba3 Mon Sep 17 00:00:00 2001 From: ChiragAgg5k Date: Tue, 14 Jan 2025 15:47:05 +0530 Subject: [PATCH 04/12] fix: not sending mail to appuser --- app/controllers/api/teams.php | 250 +++++++++++++++++----------------- 1 file changed, 127 insertions(+), 123 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 3664afec43..da6828c6bf 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -580,144 +580,148 @@ App::post('/v1/teams/:teamId/memberships') Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) : $dbForProject->createDocument('memberships', $membership); } else { - + $membership = ($isPrivilegedUser || $isAppUser) ? Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) : $dbForProject->updateDocument('memberships', $membership->getId(), $membership); } - $dbForProject->purgeCachedDocument('users', $invitee->getId()); - $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]'); + if (!$isPrivilegedUser && !$isAppUser) { + $dbForProject->purgeCachedDocument('users', $invitee->getId()); + } else { - $body = $locale->getText("emails.invitation.body"); - $subject = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName); - $customTemplate = $project->getAttribute('templates', [])['email.invitation-' . $locale->default] ?? []; + $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]'); - $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); - $message - ->setParam('{{body}}', $body, escapeHtml: false) - ->setParam('{{hello}}', $locale->getText("emails.invitation.hello")) - ->setParam('{{footer}}', $locale->getText("emails.invitation.footer")) - ->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks")) - ->setParam('{{signature}}', $locale->getText("emails.invitation.signature")); - $body = $message->render(); + $body = $locale->getText("emails.invitation.body"); + $subject = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName); + $customTemplate = $project->getAttribute('templates', [])['email.invitation-' . $locale->default] ?? []; - $smtp = $project->getAttribute('smtp', []); - $smtpEnabled = $smtp['enabled'] ?? false; + $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); + $message + ->setParam('{{body}}', $body, escapeHtml: false) + ->setParam('{{hello}}', $locale->getText("emails.invitation.hello")) + ->setParam('{{footer}}', $locale->getText("emails.invitation.footer")) + ->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks")) + ->setParam('{{signature}}', $locale->getText("emails.invitation.signature")); + $body = $message->render(); - $senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); - $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); - $replyTo = ""; + $smtp = $project->getAttribute('smtp', []); + $smtpEnabled = $smtp['enabled'] ?? false; - if ($smtpEnabled) { - if (!empty($smtp['senderEmail'])) { - $senderEmail = $smtp['senderEmail']; - } - if (!empty($smtp['senderName'])) { - $senderName = $smtp['senderName']; - } - if (!empty($smtp['replyTo'])) { - $replyTo = $smtp['replyTo']; + $senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); + $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); + $replyTo = ""; + + if ($smtpEnabled) { + if (!empty($smtp['senderEmail'])) { + $senderEmail = $smtp['senderEmail']; + } + if (!empty($smtp['senderName'])) { + $senderName = $smtp['senderName']; + } + if (!empty($smtp['replyTo'])) { + $replyTo = $smtp['replyTo']; + } + + $queueForMails + ->setSmtpHost($smtp['host'] ?? '') + ->setSmtpPort($smtp['port'] ?? '') + ->setSmtpUsername($smtp['username'] ?? '') + ->setSmtpPassword($smtp['password'] ?? '') + ->setSmtpSecure($smtp['secure'] ?? ''); + + if (!empty($customTemplate)) { + if (!empty($customTemplate['senderEmail'])) { + $senderEmail = $customTemplate['senderEmail']; + } + if (!empty($customTemplate['senderName'])) { + $senderName = $customTemplate['senderName']; + } + if (!empty($customTemplate['replyTo'])) { + $replyTo = $customTemplate['replyTo']; + } + + $body = $customTemplate['message'] ?? ''; + $subject = $customTemplate['subject'] ?? $subject; + } + + $queueForMails + ->setSmtpReplyTo($replyTo) + ->setSmtpSenderEmail($senderEmail) + ->setSmtpSenderName($senderName); } + $emailVariables = [ + 'owner' => $user->getAttribute('name'), + 'direction' => $locale->getText('settings.direction'), + /* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */ + 'user' => $user->getAttribute('name'), + 'team' => $team->getAttribute('name'), + 'redirect' => $url, + 'project' => $projectName + ]; + $queueForMails - ->setSmtpHost($smtp['host'] ?? '') - ->setSmtpPort($smtp['port'] ?? '') - ->setSmtpUsername($smtp['username'] ?? '') - ->setSmtpPassword($smtp['password'] ?? '') - ->setSmtpSecure($smtp['secure'] ?? ''); - - if (!empty($customTemplate)) { - if (!empty($customTemplate['senderEmail'])) { - $senderEmail = $customTemplate['senderEmail']; - } - if (!empty($customTemplate['senderName'])) { - $senderName = $customTemplate['senderName']; - } - if (!empty($customTemplate['replyTo'])) { - $replyTo = $customTemplate['replyTo']; - } - - $body = $customTemplate['message'] ?? ''; - $subject = $customTemplate['subject'] ?? $subject; - } - - $queueForMails - ->setSmtpReplyTo($replyTo) - ->setSmtpSenderEmail($senderEmail) - ->setSmtpSenderName($senderName); - } - - $emailVariables = [ - 'owner' => $user->getAttribute('name'), - 'direction' => $locale->getText('settings.direction'), - /* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */ - 'user' => $user->getAttribute('name'), - 'team' => $team->getAttribute('name'), - 'redirect' => $url, - 'project' => $projectName - ]; - - $queueForMails - ->setSubject($subject) - ->setBody($body) - ->setRecipient($invitee->getAttribute('email')) - ->setName($invitee->getAttribute('name')) - ->setVariables($emailVariables) - ->trigger(); - - } elseif (!empty($phone)) { - if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { - throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); - } - - $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl'); - - $customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? []; - if (!empty($customTemplate)) { - $message = $customTemplate['message']; - } - - $message = $message->setParam('{{token}}', $url); - $message = $message->render(); - - $messageDoc = new Document([ - '$id' => ID::unique(), - 'data' => [ - 'content' => $message, - ], - ]); - - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_INTERNAL) - ->setMessage($messageDoc) - ->setRecipients([$phone]) - ->setProviderType('SMS'); - - if (isset($plan['authPhone'])) { - $timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days - $timelimit - ->setParam('{organizationId}', $project->getAttribute('teamId')); - - $abuse = new Abuse($timelimit); - if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') { - $helper = PhoneNumberUtil::getInstance(); - $countryCode = $helper->parse($phone)->getCountryCode(); - - if (!empty($countryCode)) { - $queueForUsage - ->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1); - } - } - $queueForUsage - ->addMetric(METRIC_AUTH_METHOD_PHONE, 1) - ->setProject($project) + ->setSubject($subject) + ->setBody($body) + ->setRecipient($invitee->getAttribute('email')) + ->setName($invitee->getAttribute('name')) + ->setVariables($emailVariables) ->trigger(); + + } elseif (!empty($phone)) { + if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { + throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); + } + + $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl'); + + $customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? []; + if (!empty($customTemplate)) { + $message = $customTemplate['message']; + } + + $message = $message->setParam('{{token}}', $url); + $message = $message->render(); + + $messageDoc = new Document([ + '$id' => ID::unique(), + 'data' => [ + 'content' => $message, + ], + ]); + + $queueForMessaging + ->setType(MESSAGE_SEND_TYPE_INTERNAL) + ->setMessage($messageDoc) + ->setRecipients([$phone]) + ->setProviderType('SMS'); + + if (isset($plan['authPhone'])) { + $timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days + $timelimit + ->setParam('{organizationId}', $project->getAttribute('teamId')); + + $abuse = new Abuse($timelimit); + if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') { + $helper = PhoneNumberUtil::getInstance(); + $countryCode = $helper->parse($phone)->getCountryCode(); + + if (!empty($countryCode)) { + $queueForUsage + ->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1); + } + } + $queueForUsage + ->addMetric(METRIC_AUTH_METHOD_PHONE, 1) + ->setProject($project) + ->trigger(); + } } } From 860c917cf64a8242087e0c213ef492df119293fb Mon Sep 17 00:00:00 2001 From: ChiragAgg5k Date: Tue, 14 Jan 2025 15:48:28 +0530 Subject: [PATCH 05/12] fix: increment --- 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 da6828c6bf..c30251a62b 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -579,8 +579,9 @@ App::post('/v1/teams/:teamId/memberships') $membership = ($isPrivilegedUser || $isAppUser) ? Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) : $dbForProject->createDocument('memberships', $membership); - } else { + Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); + } else { $membership = ($isPrivilegedUser || $isAppUser) ? Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) : $dbForProject->updateDocument('memberships', $membership->getId(), $membership); @@ -590,7 +591,6 @@ App::post('/v1/teams/:teamId/memberships') if (!$isPrivilegedUser && !$isAppUser) { $dbForProject->purgeCachedDocument('users', $invitee->getId()); } else { - $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); From d43507040c166199d0831aece6dd24c6c54903bd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 14 Jan 2025 10:43:32 +0000 Subject: [PATCH 06/12] fix: tests --- 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 c30251a62b..feb1cb4acd 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -588,7 +588,7 @@ App::post('/v1/teams/:teamId/memberships') } - if (!$isPrivilegedUser && !$isAppUser) { + if ($isPrivilegedUser || $isAppUser) { $dbForProject->purgeCachedDocument('users', $invitee->getId()); } else { $url = Template::parseURL($url); From 120ceeedadc054ea08d095e7643049881d864eb0 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 16 Jan 2025 04:27:25 +0000 Subject: [PATCH 07/12] chore: review --- app/controllers/api/teams.php | 4 ++-- tests/e2e/Services/Teams/TeamsBaseClient.php | 5 +---- tests/e2e/Services/Teams/TeamsBaseServer.php | 5 +---- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index feb1cb4acd..39dc2868de 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -547,8 +547,8 @@ App::post('/v1/teams/:teamId/memberships') } $membership = $dbForProject->findOne('memberships', [ - Query::equal('userId', [$invitee->getId()]), - Query::equal('teamId', [$team->getId()]), + Query::equal('userId', [$invitee->getInternalId()]), + Query::equal('teamId', [$team->getInternalId()]), ]); if ($membership->isEmpty()) { diff --git a/tests/e2e/Services/Teams/TeamsBaseClient.php b/tests/e2e/Services/Teams/TeamsBaseClient.php index cf011d2bf7..3381b80120 100644 --- a/tests/e2e/Services/Teams/TeamsBaseClient.php +++ b/tests/e2e/Services/Teams/TeamsBaseClient.php @@ -291,10 +291,7 @@ trait TeamsBaseClient $this->assertEquals($secondName, $lastEmail['to'][0]['name']); $this->assertEquals('Invitation to ' . $teamName . ' Team at ' . $this->getProject()['name'], $lastEmail['subject']); - /** - * Test for resend - * SUCCESS - */ + // test for resending invitation $response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], diff --git a/tests/e2e/Services/Teams/TeamsBaseServer.php b/tests/e2e/Services/Teams/TeamsBaseServer.php index 226536c604..bade16cf2f 100644 --- a/tests/e2e/Services/Teams/TeamsBaseServer.php +++ b/tests/e2e/Services/Teams/TeamsBaseServer.php @@ -185,10 +185,7 @@ trait TeamsBaseServer // $this->assertContains('team:'.$teamUid.'/admin', $response['body']['roles']); // $this->assertContains('team:'.$teamUid.'/editor', $response['body']['roles']); - /** - * Test for resend - * SUCCESS - */ + // test for resending invitation $response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], From 23d20b7d02be0fa492d476d6cb90b57912391b98 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 16 Jan 2025 04:29:11 +0000 Subject: [PATCH 08/12] fix: invited user's name --- 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 39dc2868de..12b36b1e1b 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -660,7 +660,7 @@ App::post('/v1/teams/:teamId/memberships') 'owner' => $user->getAttribute('name'), 'direction' => $locale->getText('settings.direction'), /* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */ - 'user' => $user->getAttribute('name'), + 'user' => $name, 'team' => $team->getAttribute('name'), 'redirect' => $url, 'project' => $projectName From 8dc3b4ca6032c337e7f0e9a0f6b078d90a38e83f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 16 Jan 2025 06:15:41 +0000 Subject: [PATCH 09/12] fix: tests: --- 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 12b36b1e1b..827ca58964 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -547,8 +547,8 @@ App::post('/v1/teams/:teamId/memberships') } $membership = $dbForProject->findOne('memberships', [ - Query::equal('userId', [$invitee->getInternalId()]), - Query::equal('teamId', [$team->getInternalId()]), + Query::equal('userInternalId', [$invitee->getInternalId()]), + Query::equal('teamInternalId', [$team->getInternalId()]), ]); if ($membership->isEmpty()) { From a1031ab921ec2b2ceb876fc194e2adcabf43b13b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 17 Jan 2025 03:58:23 +0000 Subject: [PATCH 10/12] fix: update membership --- app/controllers/api/teams.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 827ca58964..7efe2a5a2d 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -582,6 +582,12 @@ App::post('/v1/teams/:teamId/memberships') Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); } else { + $membership = new Document([ + '$id' => $membership->getId(), + 'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null, + 'confirm' => ($isPrivilegedUser || $isAppUser), + ]); + $membership = ($isPrivilegedUser || $isAppUser) ? Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) : $dbForProject->updateDocument('memberships', $membership->getId(), $membership); From 1d1b83afb6f46403a45e256a56910a172bb9ad39 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 17 Jan 2025 17:27:54 +0000 Subject: [PATCH 11/12] chore: review --- app/controllers/api/teams.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 7efe2a5a2d..d98cfe6f0b 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -582,11 +582,12 @@ App::post('/v1/teams/:teamId/memberships') Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); } else { - $membership = new Document([ - '$id' => $membership->getId(), - 'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null, - 'confirm' => ($isPrivilegedUser || $isAppUser), - ]); + $membership->setAttribute('invited', DateTime::now()); + + if ($isPrivilegedUser || $isAppUser) { + $membership->setAttribute('joined', DateTime::now()); + $membership->setAttribute('confirm', true); + } $membership = ($isPrivilegedUser || $isAppUser) ? Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) : From 0408de32535e9aa226e0ff8bdde4097eec4cd685 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Mon, 20 Jan 2025 02:14:29 +0000 Subject: [PATCH 12/12] Update Fetch to 0.3.0 --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 4e1b5a98a2..1cdbe5053c 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", - "utopia-php/fetch": "0.2.*", + "utopia-php/fetch": "0.3.*", "utopia-php/image": "0.7.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.6.*", diff --git a/composer.lock b/composer.lock index 980dcdcd29..dea3ad41f6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3853435a659889e86c16764046950bed", + "content-hash": "92b40e75d531c98b72508dd104eeab0d", "packages": [ { "name": "adhocore/jwt", @@ -3639,16 +3639,16 @@ }, { "name": "utopia-php/fetch", - "version": "0.2.1", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/fetch.git", - "reference": "1423c0ee3eef944d816ca6e31706895b585aea82" + "reference": "02b12c05aec13399dcc2da8d51f908e328ab63f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/fetch/zipball/1423c0ee3eef944d816ca6e31706895b585aea82", - "reference": "1423c0ee3eef944d816ca6e31706895b585aea82", + "url": "https://api.github.com/repos/utopia-php/fetch/zipball/02b12c05aec13399dcc2da8d51f908e328ab63f4", + "reference": "02b12c05aec13399dcc2da8d51f908e328ab63f4", "shasum": "" }, "require": { @@ -3672,9 +3672,9 @@ "description": "A simple library that provides an interface for making HTTP Requests.", "support": { "issues": "https://github.com/utopia-php/fetch/issues", - "source": "https://github.com/utopia-php/fetch/tree/0.2.1" + "source": "https://github.com/utopia-php/fetch/tree/0.3.0" }, - "time": "2024-03-18T11:50:59+00:00" + "time": "2025-01-17T06:11:10+00:00" }, { "name": "utopia-php/framework",