From c97a4868b1ab997e68b44f2881d2628d4b169b2d Mon Sep 17 00:00:00 2001 From: Pranav2612000 Date: Sat, 11 Mar 2023 00:23:49 +0530 Subject: [PATCH 01/55] fix: trigger account subscription event on email verification --- app/controllers/api/account.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index aa447290bf..5b0454ded2 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2333,7 +2333,7 @@ App::put('/v1/account/verification') $dbForProject->deleteCachedDocument('users', $profile->getId()); $events - ->setParam('userId', $user->getId()) + ->setParam('userId', $userId) ->setParam('tokenId', $verificationDocument->getId()) ; From 518b45827ffd981c15944666ed00a941afcb9474 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 19 Apr 2023 02:17:26 +0000 Subject: [PATCH 02/55] content-range end should be 1 less than file size --- app/controllers/api/functions.php | 4 ++-- app/controllers/api/storage.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 5c10a3df9b..d39c637a2e 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -650,11 +650,11 @@ App::post('/v1/functions/:functionId/deployments') $end = $request->getContentRangeEnd(); $fileSize = $request->getContentRangeSize(); $deploymentId = $request->getHeader('x-appwrite-id', $deploymentId); - if (is_null($start) || is_null($end) || is_null($fileSize)) { + if (is_null($start) || is_null($end) || is_null($fileSize) || $end >= $fileSize) { throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE); } - if ($end === $fileSize) { + if ($end === $fileSize - 1) { //if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to notify it's last chunk $chunks = $chunk = -1; } else { diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index b936fc6b2f..47a4656715 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -444,11 +444,11 @@ App::post('/v1/storage/buckets/:bucketId/files') $end = $request->getContentRangeEnd(); $fileSize = $request->getContentRangeSize(); $fileId = $request->getHeader('x-appwrite-id', $fileId); - if (is_null($start) || is_null($end) || is_null($fileSize)) { + if (is_null($start) || is_null($end) || is_null($fileSize) || $end >= $fileSize) { throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE); } - if ($end === $fileSize) { + if ($end === $fileSize - 1) { //if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to -1 notify it's last chunk $chunks = $chunk = -1; } else { From 26aea21768dad1396bb004665474ddbb7eeba2ed Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 19 Apr 2023 04:44:59 +0000 Subject: [PATCH 03/55] fix chunk header size in test --- tests/e2e/Services/Storage/StorageBase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index baef838979..73bc279926 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -101,7 +101,7 @@ trait StorageBase $id = ''; while (!feof($handle)) { $curlFile = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'large-file.mp4'); - $headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size) . '/' . $size; + $headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size - 1) . '/' . $size; if (!empty($id)) { $headers['x-appwrite-id'] = $id; } @@ -145,7 +145,7 @@ trait StorageBase ]; $id = ''; $curlFile = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'large-file.mp4'); - $headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size) . '/' . $size; + $headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size - 1) . '/' . $size; $res = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucket2['body']['$id'] . '/files', $this->getHeaders(), [ 'fileId' => $fileId, 'file' => $curlFile, From 3486885dc8b54312c497cd513077e523c48c0f06 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 19 Apr 2023 05:38:54 +0000 Subject: [PATCH 04/55] fix large deployment test --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 2b66277952..37ba2ede7b 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -405,7 +405,7 @@ class FunctionsCustomServerTest extends Scope $id = ''; while (!feof($handle)) { $curlFile = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'php-large-fx.tar.gz'); - $headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size) . '/' . $size; + $headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size - 1) . '/' . $size; if (!empty($id)) { $headers['x-appwrite-id'] = $id; } From b13800386d65227e6289c0c1b17f48b0051aa892 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 19 Apr 2023 06:15:24 +0000 Subject: [PATCH 05/55] fix mock for upload changes --- app/controllers/mock.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 33e92f289a..1f251a6d35 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -286,7 +286,7 @@ App::post('/v1/mock/tests/general/upload') $id = $request->getHeader('x-appwrite-id', ''); $file['size'] = (\is_array($file['size'])) ? $file['size'][0] : $file['size']; - if (is_null($start) || is_null($end) || is_null($size)) { + if (is_null($start) || is_null($end) || is_null($size) || $end >= $size) { throw new Exception(Exception::GENERAL_MOCK, 'Invalid content-range header'); } @@ -302,11 +302,11 @@ App::post('/v1/mock/tests/general/upload') throw new Exception(Exception::GENERAL_MOCK, 'All chunked request must have id header (except first)'); } - if ($end !== $size && $end - $start + 1 !== $chunkSize) { + if ($end !== $size - 1 && $end - $start + 1 !== $chunkSize) { throw new Exception(Exception::GENERAL_MOCK, 'Chunk size must be 5MB (except last chunk)'); } - if ($end !== $size && $file['size'] !== $chunkSize) { + if ($end !== $size - 1 && $file['size'] !== $chunkSize) { throw new Exception(Exception::GENERAL_MOCK, 'Wrong chunk size'); } @@ -314,11 +314,11 @@ App::post('/v1/mock/tests/general/upload') throw new Exception(Exception::GENERAL_MOCK, 'Chunk size must be 5MB or less'); } - if ($end !== $size) { + if ($end !== $size - 1) { $response->json([ '$id' => ID::custom('newfileid'), - 'chunksTotal' => $file['size'] / $chunkSize, - 'chunksUploaded' => $start / $chunkSize + 'chunksTotal' => (int) ceil($size / ($end + 1 - $start)), + 'chunksUploaded' => ceil($start / $chunkSize) + 1 ]); } } else { From edbc52403a39ac517b8a0a346f1dd808835c5c69 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 27 Apr 2023 09:29:39 +0000 Subject: [PATCH 06/55] update so that current SDK don't fail --- app/controllers/api/functions.php | 6 ++++-- app/controllers/api/storage.php | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index d39c637a2e..7a14c675ab 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -650,11 +650,13 @@ App::post('/v1/functions/:functionId/deployments') $end = $request->getContentRangeEnd(); $fileSize = $request->getContentRangeSize(); $deploymentId = $request->getHeader('x-appwrite-id', $deploymentId); - if (is_null($start) || is_null($end) || is_null($fileSize) || $end >= $fileSize) { + // TODO make `end >= $fileSize` in next breaking version + if (is_null($start) || is_null($end) || is_null($fileSize) || $end > $fileSize) { throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE); } - if ($end === $fileSize - 1) { + // TODO remove the condition that checks `$end === $fileSize` in next breaking version + if ($end === $fileSize - 1 || $end === $fileSize) { //if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to notify it's last chunk $chunks = $chunk = -1; } else { diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 47a4656715..d97f8c18ef 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -444,11 +444,13 @@ App::post('/v1/storage/buckets/:bucketId/files') $end = $request->getContentRangeEnd(); $fileSize = $request->getContentRangeSize(); $fileId = $request->getHeader('x-appwrite-id', $fileId); - if (is_null($start) || is_null($end) || is_null($fileSize) || $end >= $fileSize) { + // TODO make `end >= $fileSize` in next breaking version + if (is_null($start) || is_null($end) || is_null($fileSize) || $end > $fileSize) { throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE); } - if ($end === $fileSize - 1) { + // TODO remove the condition that checks `$end === $fileSize` in next breaking version + if ($end === $fileSize - 1 || $end === $fileSize) { //if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to -1 notify it's last chunk $chunks = $chunk = -1; } else { From 518a37d2946108257a3d5fbbcbe1b2f182227a80 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Thu, 27 Apr 2023 19:10:42 -0700 Subject: [PATCH 07/55] Add a generic OIDC OAuth2 provider --- app/config/providers.php | 10 + src/Appwrite/Auth/OAuth2/Oidc.php | 296 ++++++++++++++++++++++++++++++ 2 files changed, 306 insertions(+) create mode 100644 src/Appwrite/Auth/OAuth2/Oidc.php diff --git a/app/config/providers.php b/app/config/providers.php index 62430faae1..5e4dc0aa11 100644 --- a/app/config/providers.php +++ b/app/config/providers.php @@ -201,6 +201,16 @@ return [ // Ordered by ABC. 'beta' => false, 'mock' => false, ], + 'oidc' => [ + 'name' => 'OpenID Connect', + 'developers' => 'https://openid.net/connect/faq/', + 'icon' => 'icon-oidc', + 'enabled' => true, + 'sandbox' => false, + 'form' => 'oidc.phtml', + 'beta' => false, + 'mock' => false, + ], 'okta' => [ 'name' => 'Okta', 'developers' => 'https://developer.okta.com/', diff --git a/src/Appwrite/Auth/OAuth2/Oidc.php b/src/Appwrite/Auth/OAuth2/Oidc.php new file mode 100644 index 0000000000..de2eab65c8 --- /dev/null +++ b/src/Appwrite/Auth/OAuth2/Oidc.php @@ -0,0 +1,296 @@ +getAuthorizationEndpoint() . '?' . \http_build_query([ + 'client_id' => $this->appID, + 'redirect_uri' => $this->callback, + 'state' => \json_encode($this->state), + 'scope' => \implode(' ', $this->getScopes()), + 'response_type' => 'code', + ]); + } + + /** + * @param string $code + * + * @return array + */ + protected function getTokens(string $code): array + { + if (empty($this->tokens)) { + $headers = ['Content-Type: application/x-www-form-urlencoded']; + $this->tokens = \json_decode($this->request( + 'POST', + $this->getTokenEndpoint(), + $headers, + \http_build_query([ + 'code' => $code, + 'client_id' => $this->appID, + 'client_secret' => $this->getClientSecret(), + 'redirect_uri' => $this->callback, + 'scope' => \implode(' ', $this->getScopes()), + 'grant_type' => 'authorization_code' + ]) + ), true); + } + return $this->tokens; + } + + + /** + * @param string $refreshToken + * + * @return array + */ + public function refreshTokens(string $refreshToken): array + { + $headers = ['Content-Type: application/x-www-form-urlencoded']; + $this->tokens = \json_decode($this->request( + 'POST', + $this->getTokenEndpoint(), + $headers, + \http_build_query([ + 'refresh_token' => $refreshToken, + 'client_id' => $this->appID, + 'client_secret' => $this->getClientSecret(), + 'grant_type' => 'refresh_token' + ]) + ), true); + + if (empty($this->tokens['refresh_token'])) { + $this->tokens['refresh_token'] = $refreshToken; + } + + return $this->tokens; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserID(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if (isset($user['sub'])) { + return $user['sub']; + } + + return ''; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserEmail(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if (isset($user['email'])) { + return $user['email']; + } + + return ''; + } + + /** + * Check if the User email is verified + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + return $user['email_verified'] ?? false; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserName(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if (isset($user['name'])) { + return $user['name']; + } + + return ''; + } + + /** + * @param string $accessToken + * + * @return array + */ + protected function getUser(string $accessToken): array + { + if (empty($this->user)) { + $headers = ['Authorization: Bearer ' . \urlencode($accessToken)]; + $user = $this->request('GET', $this->getUserinfoEndpoint(), $headers); + $this->user = \json_decode($user, true); + } + + return $this->user; + } + + /** + * Extracts the Client Secret from the JSON stored in appSecret + * + * @return string + */ + protected function getClientSecret(): string + { + $secret = $this->getAppSecret(); + + return $secret['clientSecret'] ?? ''; + } + + /** + * Extracts the well known endpoint from the JSON stored in appSecret. + * + * @return string + */ + protected function getWellKnownEndpoint(): string + { + $secret = $this->getAppSecret(); + return $secret['wellKnownEndpoint'] ?? ''; + } + + /** + * Extracts the authorization endpoint from the JSON stored in appSecret. + * + * If one is not provided, it will be retrieved from the well-known configuration. + * + * @return string + */ + protected function getAuthorizationEndpoint(): string + { + $secret = $this->getAppSecret(); + + $endpoint = $secret['authorizationEndpoint'] ?? ''; + if (!empty($endpoint)) { + return $endpoint; + } + + $wellKnownConfiguration = $this->getWellKnownConfiguration(); + return $wellKnownConfiguration['authorization_endpoint'] ?? ''; + } + + /** + * Extracts the token endpoint from the JSON stored in appSecret. + * + * If one is not provided, it will be retrieved from the well-known configuration. + * + * @return string + */ + protected function getTokenEndpoint(): string + { + $secret = $this->getAppSecret(); + + $endpoint = $secret['tokenEndpoint'] ?? ''; + if (!empty($endpoint)) { + return $endpoint; + } + + $wellKnownConfiguration = $this->getWellKnownConfiguration(); + return $wellKnownConfiguration['token_endpoint'] ?? ''; + } + + /** + * Extracts the userinfo endpoint from the JSON stored in appSecret. + * + * If one is not provided, it will be retrieved from the well-known configuration. + * + * @return string + */ + protected function getUserinfoEndpoint(): string + { + $secret = $this->getAppSecret(); + $endpoint = $secret['userinfoEndpoint'] ?? ''; + if (!empty($endpoint)) { + return $endpoint; + } + + $wellKnownConfiguration = $this->getWellKnownConfiguration(); + return $wellKnownConfiguration['userinfo_endpoint'] ?? ''; + } + + /** + * Get the well-known configuration using the well known endpoint + */ + protected function getWellKnownConfiguration(): array + { + if (empty($this->wellKnownConfiguration)) { + $response = $this->request('GET', $this->getWellKnownEndpoint()); + $this->wellKnownConfiguration = \json_decode($response, true); + } + + return $this->wellKnownConfiguration; + } + + /** + * Decode the JSON stored in appSecret + * + * @return array + */ + protected function getAppSecret(): array + { + try { + $secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR); + } catch (\Throwable $th) { + throw new \Exception('Invalid secret'); + } + return $secret; + } +} From 582698bd03235a7ee1b458865bd0b32fa368d369 Mon Sep 17 00:00:00 2001 From: Suven-p Date: Thu, 18 May 2023 00:41:07 +0545 Subject: [PATCH 08/55] Validate value of x-appwrite-id header --- app/config/errors.php | 5 +++++ app/controllers/api/storage.php | 5 +++++ src/Appwrite/Extend/Exception.php | 1 + 3 files changed, 11 insertions(+) diff --git a/app/config/errors.php b/app/config/errors.php index 1361207a1d..078a620168 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -318,6 +318,11 @@ return [ 'description' => 'The requested range is not satisfiable. Please check the value of the Range header.', 'code' => 416, ], + Exception::STORAGE_INVALID_APPWRITE_ID => [ + 'name' => Exception::STORAGE_INVALID_APPWRITE_ID, + 'description' => 'The value for x-appwrite-id header is invalid. Please check the value of the x-appwrite-id header is valid id and not unique().', + 'code' => 400, + ], /** Functions */ Exception::FUNCTION_NOT_FOUND => [ diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index d59d950d93..b8a249f654 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -449,6 +449,11 @@ App::post('/v1/storage/buckets/:bucketId/files') throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE); } + $idValidator = new UID(); + if (!$idValidator->isValid($request->getHeader('x-appwrite-id'))) { + throw new Exception(Exception::STORAGE_INVALID_APPWRITE_ID); + } + if ($end === $fileSize) { //if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to -1 notify it's last chunk $chunks = $chunk = -1; diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index ed7194125c..05caa75c63 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -106,6 +106,7 @@ class Exception extends \Exception public const STORAGE_BUCKET_NOT_FOUND = 'storage_bucket_not_found'; public const STORAGE_INVALID_CONTENT_RANGE = 'storage_invalid_content_range'; public const STORAGE_INVALID_RANGE = 'storage_invalid_range'; + public const STORAGE_INVALID_APPWRITE_ID = 'storage_invalid_appwrite_id'; /** Functions */ public const FUNCTION_NOT_FOUND = 'function_not_found'; From 5cb9cc9dfd5d3aece907a4739ef5367824e52a32 Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Sat, 20 May 2023 02:36:45 +0530 Subject: [PATCH 09/55] fix expire field in list account sessions api --- app/controllers/api/account.php | 1 + tests/e2e/Services/Account/AccountBase.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index dd5ac4a2da..5108091b36 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1389,6 +1389,7 @@ App::get('/v1/account/sessions') $session->setAttribute('countryName', $countryName); $session->setAttribute('current', ($current == $session->getId()) ? true : false); + $session->setAttribute('expire', DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration)); $sessions[$key] = $session; } diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index ba751516d3..c4401d6c6f 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -308,6 +308,8 @@ trait AccountBase $this->assertEquals(true, $response['body']['sessions'][0]['current']); + $this->assertEquals(true, $response['body']['sessions'][0]['expire']); + /** * Test for FAILURE */ From 19a3db69f8e43596257b0d11752de53f1e7357a4 Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Sat, 20 May 2023 21:09:48 +0530 Subject: [PATCH 10/55] fix updated expire format to iso format --- app/controllers/api/account.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 5108091b36..a91c784df5 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1389,7 +1389,7 @@ App::get('/v1/account/sessions') $session->setAttribute('countryName', $countryName); $session->setAttribute('current', ($current == $session->getId()) ? true : false); - $session->setAttribute('expire', DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration)); + $session->setAttribute('expire', DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration))); $sessions[$key] = $session; } From 6858c5c26ace1272655088d9b7ec02a5a08e6ae7 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 21 May 2023 07:27:20 +0545 Subject: [PATCH 11/55] Update GETTING_STARTED.md --- docs/sdks/flutter/GETTING_STARTED.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sdks/flutter/GETTING_STARTED.md b/docs/sdks/flutter/GETTING_STARTED.md index b9b8dc3a47..110ee3eb4a 100644 --- a/docs/sdks/flutter/GETTING_STARTED.md +++ b/docs/sdks/flutter/GETTING_STARTED.md @@ -46,6 +46,8 @@ For **Linux** add your app name and package name, Your package nam ### Mac OS For **Mac OS** add your app name and Bundle ID, You can find your Bundle Identifier in the General tab for your app's primary target in Xcode. +The Appwrite SDK uses ASWebAuthenticationSession on macOS 10.15+ to allow OAuth authentication. You have to change your macOS Deployment Target in Xcode to be macOS >= 10.15 to be able to build your app for macOS. + ### Web Appwrite 0.7, and the Appwrite Flutter SDK 0.3.0 have added support for Flutter Web. To build web apps that integrate with Appwrite successfully, all you have to do is add a web platform on your Appwrite project's dashboard and list the domain your website will use to allow communication to the Appwrite API. From 5108ab7b061ffb1846b1cbdbd3077110e026482c Mon Sep 17 00:00:00 2001 From: Suven-p Date: Sun, 21 May 2023 15:43:26 +0000 Subject: [PATCH 12/55] Add test for x-appwrite-id = unique() --- tests/e2e/Services/Storage/StorageBase.php | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index 3169077f99..88a09fe66f 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -240,6 +240,29 @@ trait StorageBase $this->assertEquals(400, $failedBucket['headers']['status-code']); + /** + * Test for FAILURE set x-appwrite-id to unique() + */ + $source = realpath(__DIR__ . '/../../../resources/logo.png'); + $totalSize = \filesize($source); + $res = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + 'content-range' => 'bytes 0-' . $size. '/' . $size, + 'x-appwrite-id' => 'unique()', + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile($source, 'image/png', 'logo.png'), + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(400, $res['headers']['status-code']); + $this->assertEquals('The value for x-appwrite-id header is invalid. Please check the value of the x-appwrite-id header is valid id and not unique().', $res['body']['message']); + return ['bucketId' => $bucketId, 'fileId' => $file['body']['$id'], 'largeFileId' => $largeFile['body']['$id'], 'largeBucketId' => $bucket2['body']['$id']]; } From 8673df4ac5744b8a0cbc3faad9f3836a247c797b Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Mon, 22 May 2023 17:52:25 +0530 Subject: [PATCH 13/55] fix: expire format in magic url sessions --- app/controllers/api/account.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index dd5ac4a2da..a8c4cad5aa 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -701,7 +701,7 @@ App::post('/v1/account/sessions/magic-url') } $loginSecret = Auth::tokenGenerator(); - $expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM); + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM)); $token = new Document([ '$id' => ID::unique(), @@ -827,7 +827,7 @@ App::put('/v1/account/sessions/magic-url') $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); $secret = Auth::tokenGenerator(); - $expire = DateTime::addSeconds(new \DateTime(), $duration); + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $session = new Document(array_merge( [ From 249bec5124382a59aa9130e6a5e119c65d28c22e Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Mon, 22 May 2023 18:18:52 +0530 Subject: [PATCH 14/55] fixed expire format for phone session, anonymous session, oauth session --- app/controllers/api/account.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index dd5ac4a2da..0c5a358307 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -549,7 +549,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); $secret = Auth::tokenGenerator(); - $expire = DateTime::addSeconds(new \DateTime(), $duration); + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $session = new Document(array_merge([ '$id' => ID::unique(), @@ -976,7 +976,7 @@ App::post('/v1/account/sessions/phone') } $secret = Auth::codeGenerator(); - $expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_PHONE); + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_PHONE)); $token = new Document([ '$id' => ID::unique(), @@ -1064,7 +1064,7 @@ App::put('/v1/account/sessions/phone') $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); $secret = Auth::tokenGenerator(); - $expire = DateTime::addSeconds(new \DateTime(), $duration); + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $session = new Document(array_merge( [ @@ -1216,7 +1216,7 @@ App::post('/v1/account/sessions/anonymous') $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); $secret = Auth::tokenGenerator(); - $expire = DateTime::addSeconds(new \DateTime(), $duration); + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $session = new Document(array_merge( [ From 977279b6792a7bf4fb6a4a89417b6fe5ae676e75 Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Mon, 22 May 2023 18:34:51 +0530 Subject: [PATCH 15/55] fix: expire format for get session api --- app/controllers/api/account.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index dd5ac4a2da..a6f98aa1dc 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1495,7 +1495,7 @@ App::get('/v1/account/sessions/:sessionId') $session ->setAttribute('current', ($session->getAttribute('secret') == Auth::hash(Auth::$secret))) ->setAttribute('countryName', $countryName) - ->setAttribute('expire', DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration)) + ->setAttribute('expire', DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration))) ; return $response->dynamic($session, Response::MODEL_SESSION); @@ -1932,7 +1932,7 @@ App::patch('/v1/account/sessions/:sessionId') $authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; - $session->setAttribute('expire', DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration)); + $session->setAttribute('expire', DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration))); $events ->setParam('userId', $user->getId()) From 42b2587b6da7eaf8bf17b7519d7efc4aeba18d7b Mon Sep 17 00:00:00 2001 From: Suven-p Date: Sat, 27 May 2023 22:54:00 +0545 Subject: [PATCH 16/55] Fix linting error --- tests/e2e/Services/Storage/StorageBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index 88a09fe66f..62b49e62f3 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -248,7 +248,7 @@ trait StorageBase $res = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], - 'content-range' => 'bytes 0-' . $size. '/' . $size, + 'content-range' => 'bytes 0-' . $size . '/' . $size, 'x-appwrite-id' => 'unique()', ], $this->getHeaders()), [ 'fileId' => ID::unique(), From 5dc65ea374bdc8f0c42bc52b1daf927c5a2bdb0a Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Fri, 2 Jun 2023 15:15:06 +0530 Subject: [PATCH 17/55] fix updated test case to check if expire is valid iso string or not --- tests/e2e/Services/Account/AccountBase.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index c4401d6c6f..b08686fa48 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -308,8 +308,7 @@ trait AccountBase $this->assertEquals(true, $response['body']['sessions'][0]['current']); - $this->assertEquals(true, $response['body']['sessions'][0]['expire']); - + $this->assertNotFalse(\DateTime::createFromFormat('Y-m-d\TH:i:s.uP', $response['body']['sessions'][0]['expire'])); /** * Test for FAILURE */ From 39ea3adc9d18bf9e79c591eb37456c8bb25d542c Mon Sep 17 00:00:00 2001 From: Migueli Date: Mon, 12 Jun 2023 23:39:00 +0100 Subject: [PATCH 18/55] Added missing word to portuguese email template --- app/config/locale/translations/pt-pt.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/locale/translations/pt-pt.json b/app/config/locale/translations/pt-pt.json index e257b47281..cf9ef377a8 100644 --- a/app/config/locale/translations/pt-pt.json +++ b/app/config/locale/translations/pt-pt.json @@ -17,7 +17,7 @@ "emails.magicSession.signature": "Equipa {{project}}", "emails.recovery.subject": "Redefinição de senha", "emails.recovery.hello": "Olá {{name}}", - "emails.recovery.body": "tilize este link para redefinir a palavra-passe do seu projecto {{project}}", + "emails.recovery.body": "Utilize este link para redefinir a palavra-passe do seu projecto {{project}}", "emails.recovery.footer": "Se não pediu para redefinir a sua palavra-passe, pode ignorar esta mensagem.", "emails.recovery.thanks": "Obrigado", "emails.recovery.signature": "Equipa {{project}}", From 6edea596b49ab183f7779cb1d10e25fed18cbdfe Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 14 Jun 2023 17:49:52 +0000 Subject: [PATCH 19/55] Update delete session description to be actually correct --- docs/references/account/delete-session.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/account/delete-session.md b/docs/references/account/delete-session.md index cd1f22f627..78d43cf890 100644 --- a/docs/references/account/delete-session.md +++ b/docs/references/account/delete-session.md @@ -1 +1 @@ -Use this endpoint to log out the currently logged in user from all their account sessions across all of their different devices. When using the Session ID argument, only the unique session ID provided is deleted. +Logout the current user. Use 'current' as the session ID to logout on this device, or provide a specific session ID to target a different session. \ No newline at end of file From 57621de57d47828f1aa45faa85362f63d8242d38 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 15 Jun 2023 18:38:15 +0000 Subject: [PATCH 20/55] Damodar's comments --- docs/references/account/delete-session.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/account/delete-session.md b/docs/references/account/delete-session.md index 78d43cf890..c7439638af 100644 --- a/docs/references/account/delete-session.md +++ b/docs/references/account/delete-session.md @@ -1 +1 @@ -Logout the current user. Use 'current' as the session ID to logout on this device, or provide a specific session ID to target a different session. \ No newline at end of file +Logout the user. Use 'current' as the session ID to logout on this device, use a session ID to logout on another device. If you're looking to logout the user on all devices, use [Delete Sessions](/docs/client/account#accountDeleteSessions) instead. \ No newline at end of file From fecde6bdba0af4f9e85b1b65b1490ba827610db9 Mon Sep 17 00:00:00 2001 From: vaibhavagarwal220 Date: Sat, 24 Jun 2023 13:57:02 +0530 Subject: [PATCH 21/55] fixed null $name error for anonymous user --- app/controllers/api/account.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index cc598a9e2b..78d65c669a 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2306,7 +2306,7 @@ App::post('/v1/account/verification') ->setBody($body) ->setFrom($from) ->setRecipient($user->getAttribute('email')) - ->setName($user->getAttribute('name')) + ->setName($user->getAttribute('name') ?? '') ->trigger() ; From 70cebe4460c277e5f594f1ecc16de79636f5723a Mon Sep 17 00:00:00 2001 From: Mendel Gordon Date: Thu, 29 Jun 2023 10:32:03 -0400 Subject: [PATCH 22/55] Update create-magic-url-session.md Fix typo --- docs/references/account/create-magic-url-session.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/references/account/create-magic-url-session.md b/docs/references/account/create-magic-url-session.md index 03383713ed..b1e63e144c 100644 --- a/docs/references/account/create-magic-url-session.md +++ b/docs/references/account/create-magic-url-session.md @@ -1,3 +1,3 @@ -Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [PUT /account/sessions/magic-url](/docs/client/account#accountUpdateMagicURLSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default. +Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [PUT /account/sessions/magic-url](/docs/client/account#accountUpdateMagicURLSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default. -A user is limited to 10 active sessions at a time by default. [Learn more about session limits](/docs/authentication-security#limits). \ No newline at end of file +A user is limited to 10 active sessions at a time by default. [Learn more about session limits](/docs/authentication-security#limits). From 4e24b7eb42725292006505d3a1924abfd62e5698 Mon Sep 17 00:00:00 2001 From: vaibhavagarwal220 Date: Fri, 7 Jul 2023 00:20:13 +0530 Subject: [PATCH 23/55] update e2e test for anonymous user verification --- .../e2e/Services/Account/AccountCustomClientTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index c0435762fe..3e4c9bebe6 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -494,6 +494,18 @@ class AccountCustomClientTest extends Scope 'password' => $password, ]); + $this->assertEquals($response['headers']['status-code'], 201); + + $response = $this->client->call(Client::METHOD_POST, '/account/verification', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ]), [ + 'url' => 'http://localhost' + ]); + + $this->assertEquals($response['headers']['status-code'], 201); return []; From a6671a5caada60b4dff2ba2abfff44973a85d60c Mon Sep 17 00:00:00 2001 From: jaivix Date: Sun, 9 Jul 2023 16:50:09 +0530 Subject: [PATCH 24/55] fixed team creation response if teamId already exists --- app/config/errors.php | 5 +++++ app/controllers/api/teams.php | 29 +++++++++++++++----------- src/Appwrite/Extend/Exception.php | 1 + tests/e2e/Services/Teams/TeamsBase.php | 11 ++++++++++ 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/app/config/errors.php b/app/config/errors.php index 2983d22cd8..1a93859457 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -227,6 +227,11 @@ return [ 'description' => 'The invite does not belong to the current user.', 'code' => 401, ], + Exception::TEAM_ID_CONFLICT => [ + 'name' => Exception::TEAM_ID_CONFLICT, + 'description' => 'Team ID already existed.', + 'code' => 409, + ], /** Membership */ Exception::MEMBERSHIP_NOT_FOUND => [ diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 338af70406..96b11c33dd 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -67,18 +67,23 @@ App::post('/v1/teams') $isAppUser = Auth::isAppUser(Authorization::getRoles()); $teamId = $teamId == 'unique()' ? ID::unique() : $teamId; - $team = Authorization::skip(fn() => $dbForProject->createDocument('teams', new Document([ - '$id' => $teamId, - '$permissions' => [ - Permission::read(Role::team($teamId)), - Permission::update(Role::team($teamId, 'owner')), - Permission::delete(Role::team($teamId, 'owner')), - ], - 'name' => $name, - 'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1, - 'prefs' => new \stdClass(), - 'search' => implode(' ', [$teamId, $name]), - ]))); + + try { + $team = Authorization::skip(fn() => $dbForProject->createDocument('teams', new Document([ + '$id' => $teamId, + '$permissions' => [ + Permission::read(Role::team($teamId)), + Permission::update(Role::team($teamId, 'owner')), + Permission::delete(Role::team($teamId, 'owner')), + ], + 'name' => $name, + 'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1, + 'prefs' => new \stdClass(), + 'search' => implode(' ', [$teamId, $name]), + ]))); + } catch (Duplicate $th) { + throw new Exception(Exception::TEAM_ID_CONFLICT); + } if (!$isPrivilegedUser && !$isAppUser) { // Don't add user on server mode if (!\in_array('owner', $roles)) { diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 7ed022b452..9e23c1cc52 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -83,6 +83,7 @@ class Exception extends \Exception public const TEAM_INVALID_SECRET = 'team_invalid_secret'; public const TEAM_MEMBERSHIP_MISMATCH = 'team_membership_mismatch'; public const TEAM_INVITE_MISMATCH = 'team_invite_mismatch'; + public const TEAM_ID_CONFLICT = 'team_id_conflict'; /** Membership */ public const MEMBERSHIP_NOT_FOUND = 'membership_not_found'; diff --git a/tests/e2e/Services/Teams/TeamsBase.php b/tests/e2e/Services/Teams/TeamsBase.php index 04b5f93691..3abb312a23 100644 --- a/tests/e2e/Services/Teams/TeamsBase.php +++ b/tests/e2e/Services/Teams/TeamsBase.php @@ -79,6 +79,17 @@ trait TeamsBase $this->assertEquals(400, $response['headers']['status-code']); + $response = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => $teamId, + 'name' => 'John' + ]); + + $this->assertEquals(409, $response['headers']['status-code']); + $this->assertEquals('team_id_conflict', $response['body']['type']); + return ['teamUid' => $teamUid, 'teamName' => $teamName]; } From 48fc58d365a3f1e641c4b79f368b812f7cdd2283 Mon Sep 17 00:00:00 2001 From: vaibhavagarwal220 Date: Tue, 11 Jul 2023 09:33:02 +0530 Subject: [PATCH 25/55] fixed linting errors --- tests/e2e/Services/Account/AccountCustomClientTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 3e4c9bebe6..6e9d2b6dbb 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -500,12 +500,12 @@ class AccountCustomClientTest extends Scope 'origin' => 'http://localhost', 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, ]), [ 'url' => 'http://localhost' ]); - + $this->assertEquals($response['headers']['status-code'], 201); return []; From 4db919b42dc4f7199ce19b2675d2b51f25e1b12d Mon Sep 17 00:00:00 2001 From: jaivix Date: Thu, 13 Jul 2023 00:33:38 +0530 Subject: [PATCH 26/55] linting and wording updates based on stnguyen90 feedback --- app/config/errors.php | 6 +++--- app/controllers/api/teams.php | 4 ++-- src/Appwrite/Extend/Exception.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/config/errors.php b/app/config/errors.php index 1a93859457..e71fb19547 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -227,9 +227,9 @@ return [ 'description' => 'The invite does not belong to the current user.', 'code' => 401, ], - Exception::TEAM_ID_CONFLICT => [ - 'name' => Exception::TEAM_ID_CONFLICT, - 'description' => 'Team ID already existed.', + Exception::TEAM_ALREADY_EXISTS => [ + 'name' => Exception::TEAM_ALREADY_EXISTS, + 'description' => 'Team with requested ID already exists.', 'code' => 409, ], diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 96b11c33dd..cdac9ba2fb 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -67,7 +67,7 @@ App::post('/v1/teams') $isAppUser = Auth::isAppUser(Authorization::getRoles()); $teamId = $teamId == 'unique()' ? ID::unique() : $teamId; - + try { $team = Authorization::skip(fn() => $dbForProject->createDocument('teams', new Document([ '$id' => $teamId, @@ -82,7 +82,7 @@ App::post('/v1/teams') 'search' => implode(' ', [$teamId, $name]), ]))); } catch (Duplicate $th) { - throw new Exception(Exception::TEAM_ID_CONFLICT); + throw new Exception(Exception::TEAM_ALREADY_EXISTS); } if (!$isPrivilegedUser && !$isAppUser) { // Don't add user on server mode diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 9e23c1cc52..42f8876d69 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -83,7 +83,7 @@ class Exception extends \Exception public const TEAM_INVALID_SECRET = 'team_invalid_secret'; public const TEAM_MEMBERSHIP_MISMATCH = 'team_membership_mismatch'; public const TEAM_INVITE_MISMATCH = 'team_invite_mismatch'; - public const TEAM_ID_CONFLICT = 'team_id_conflict'; + public const TEAM_ALREADY_EXISTS = 'team_already_exists'; /** Membership */ public const MEMBERSHIP_NOT_FOUND = 'membership_not_found'; From b78401923f42bef08f1381454f13dbcc91071d39 Mon Sep 17 00:00:00 2001 From: jaivix Date: Thu, 13 Jul 2023 12:10:52 +0530 Subject: [PATCH 27/55] test case minor fix --- tests/e2e/Services/Teams/TeamsBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Teams/TeamsBase.php b/tests/e2e/Services/Teams/TeamsBase.php index 3abb312a23..8e19bede17 100644 --- a/tests/e2e/Services/Teams/TeamsBase.php +++ b/tests/e2e/Services/Teams/TeamsBase.php @@ -88,7 +88,7 @@ trait TeamsBase ]); $this->assertEquals(409, $response['headers']['status-code']); - $this->assertEquals('team_id_conflict', $response['body']['type']); + $this->assertEquals('team_already_exists', $response['body']['type']); return ['teamUid' => $teamUid, 'teamName' => $teamName]; } From 131e9658a5ef644e3d422f554d1ae0e5601d561c Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Wed, 19 Jul 2023 14:04:35 +0530 Subject: [PATCH 28/55] change to use findOne instead of iterating index array --- app/controllers/api/databases.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 4459322baf..eb37bea9f0 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2535,20 +2535,18 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $indexes = $collection->getAttribute('indexes'); - - // Search for index - $indexIndex = array_search($key, array_map(fn($idx) => $idx['key'], $indexes)); - - if ($indexIndex === false) { + $index = $dbForProject->findOne('indexes', [ + Query::equal('$id',[$database->getInternalId().'_'.$collection->getInternalId().'_'.$key]) + ]); + + if ($index->isEmpty()) { throw new Exception(Exception::INDEX_NOT_FOUND); } - $index = $indexes[$indexIndex]; $index->setAttribute('collectionId', $database->getInternalId() . '_' . $collectionId); $response->dynamic($index, Response::MODEL_INDEX); - }); + });; App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) From 8328dac86bb3c6d3010eedaa7063c8cab77af565 Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Wed, 19 Jul 2023 14:08:02 +0530 Subject: [PATCH 29/55] lint issues --- app/controllers/api/databases.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index eb37bea9f0..37694fcb65 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2536,9 +2536,9 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') } $index = $dbForProject->findOne('indexes', [ - Query::equal('$id',[$database->getInternalId().'_'.$collection->getInternalId().'_'.$key]) + Query::equal('$id', [$database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key]) ]); - + if ($index->isEmpty()) { throw new Exception(Exception::INDEX_NOT_FOUND); } @@ -2546,7 +2546,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') $index->setAttribute('collectionId', $database->getInternalId() . '_' . $collectionId); $response->dynamic($index, Response::MODEL_INDEX); - });; + }); +; App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) From 803d653e44ea0a9118a34cbc0df6c53852828bfe Mon Sep 17 00:00:00 2001 From: Steven <1477010+stnguyen90@users.noreply.github.com> Date: Wed, 19 Jul 2023 18:29:01 +0000 Subject: [PATCH 30/55] Upgrade utopia-php/domains Version 1.1.0 was an incorrectly named git tag; it was equivalent to 0.2.0. This bump to 0.3.* is safe because it adds Registrar, but it won't be used in Appwrite yet. Bumping to 0.3.* is necessary to fix some bugs where valid domains were considered invalid. --- composer.json | 2 +- composer.lock | 36 +++++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/composer.json b/composer.json index 0ba26540bd..8560f450bc 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "utopia-php/cli": "0.13.*", "utopia-php/config": "0.2.*", "utopia-php/database": "0.36.*", - "utopia-php/domains": "1.1.*", + "utopia-php/domains": "0.3.*", "utopia-php/framework": "0.28.*", "utopia-php/image": "0.5.*", "utopia-php/locale": "0.4.*", diff --git a/composer.lock b/composer.lock index 0c6b961ea2..303130bea5 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": "3eadbfe5543aafdf8682ea0465159e3c", + "content-hash": "21b8661eef2ca7648831651b99a11544", "packages": [ { "name": "adhocore/jwt", @@ -2164,23 +2164,25 @@ }, { "name": "utopia-php/domains", - "version": "v1.1.0", + "version": "0.3.2", "source": { "type": "git", "url": "https://github.com/utopia-php/domains.git", - "reference": "1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73" + "reference": "aaa8c9a96c69ccb397997b1f4f2299c66f77eefb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/domains/zipball/1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73", - "reference": "1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73", + "url": "https://api.github.com/repos/utopia-php/domains/zipball/aaa8c9a96c69ccb397997b1f4f2299c66f77eefb", + "reference": "aaa8c9a96c69ccb397997b1f4f2299c66f77eefb", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.0", + "utopia-php/framework": "0.*.*" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3" }, "type": "library", "autoload": { @@ -2196,6 +2198,10 @@ { "name": "Eldad Fux", "email": "eldad@appwrite.io" + }, + { + "name": "Wess Cope", + "email": "wess@appwrite.io" } ], "description": "Utopia Domains library is simple and lite library for parsing web domains. This library is aiming to be as simple and easy to learn and use.", @@ -2212,9 +2218,9 @@ ], "support": { "issues": "https://github.com/utopia-php/domains/issues", - "source": "https://github.com/utopia-php/domains/tree/master" + "source": "https://github.com/utopia-php/domains/tree/0.3.2" }, - "time": "2020-02-23T07:40:02+00:00" + "time": "2023-07-19T16:39:24+00:00" }, { "name": "utopia-php/framework", @@ -3031,16 +3037,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.33.6", + "version": "0.33.7", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "237fe97b68090a244382c36f96482c352880a38c" + "reference": "9f5db4a637b23879ceacea9ed2d33b0486771ffc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/237fe97b68090a244382c36f96482c352880a38c", - "reference": "237fe97b68090a244382c36f96482c352880a38c", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/9f5db4a637b23879ceacea9ed2d33b0486771ffc", + "reference": "9f5db4a637b23879ceacea9ed2d33b0486771ffc", "shasum": "" }, "require": { @@ -3076,9 +3082,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.33.6" + "source": "https://github.com/appwrite/sdk-generator/tree/0.33.7" }, - "time": "2023-07-10T16:27:53+00:00" + "time": "2023-07-12T12:15:43+00:00" }, { "name": "doctrine/deprecations", From cc42700299b307893ae429e43aaf5e5a036bb915 Mon Sep 17 00:00:00 2001 From: Steven <1477010+stnguyen90@users.noreply.github.com> Date: Wed, 19 Jul 2023 23:52:33 +0000 Subject: [PATCH 31/55] Fix test after session expire format was updated --- tests/e2e/Services/Account/AccountBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index b08686fa48..f6d10653d8 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -1296,7 +1296,7 @@ trait AccountBase $token = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256); - $expireTime = strpos($lastEmail['text'], 'expire=' . urlencode(DateTime::format(new \DateTime($response['body']['expire']))), 0); + $expireTime = strpos($lastEmail['text'], 'expire=' . urlencode($response['body']['expire']), 0); $this->assertNotFalse($expireTime); From 35a25cc2a74e1e33241c3fe85a9ca45705d1e133 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 12 Jul 2023 10:06:12 -0700 Subject: [PATCH 32/55] Bump audit version to get rid of userInternalId attribute --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 8560f450bc..b02a6111bc 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "appwrite/php-runtimes": "0.11.*", "utopia-php/abuse": "0.25.*", "utopia-php/analytics": "0.2.*", - "utopia-php/audit": "0.26.*", + "utopia-php/audit": "0.27.*", "utopia-php/cache": "0.8.*", "utopia-php/cli": "0.13.*", "utopia-php/config": "0.2.*", diff --git a/composer.lock b/composer.lock index 303130bea5..5e6640b9fb 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": "21b8661eef2ca7648831651b99a11544", + "content-hash": "a15f62920dedaf99e321ffd94e89d9f4", "packages": [ { "name": "adhocore/jwt", @@ -1906,16 +1906,16 @@ }, { "name": "utopia-php/audit", - "version": "0.26.0", + "version": "0.27.0", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "e7228080f14df28737fbb050c180c26d86ac0403" + "reference": "bdf89d7fe381bd4c891ad217612580a35e8c7642" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/e7228080f14df28737fbb050c180c26d86ac0403", - "reference": "e7228080f14df28737fbb050c180c26d86ac0403", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/bdf89d7fe381bd4c891ad217612580a35e8c7642", + "reference": "bdf89d7fe381bd4c891ad217612580a35e8c7642", "shasum": "" }, "require": { @@ -1947,9 +1947,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.26.0" + "source": "https://github.com/utopia-php/audit/tree/0.27.0" }, - "time": "2023-04-27T15:43:50+00:00" + "time": "2023-05-15T07:04:48+00:00" }, { "name": "utopia-php/cache", From 58fa7546c15c878ffea2a0e8bd87a302056c5bf3 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 12 Jul 2023 10:18:34 -0700 Subject: [PATCH 33/55] Update Appwrite to save internal id as audit user id It's important to use userInternalId so that if a user is recreated with the same ID, lookups for the user will not return the data of the old deleted user. We will still store userId in data so that it can be pulled out and returned for the log API calls. --- app/workers/audits.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/workers/audits.php b/app/workers/audits.php index b25430ec41..57614e60f6 100644 --- a/app/workers/audits.php +++ b/app/workers/audits.php @@ -40,8 +40,7 @@ class AuditsV1 extends Worker $dbForProject = $this->getProjectDB($project->getId()); $audit = new Audit($dbForProject); $audit->log( - userInternalId: $user->getInternalId(), - userId: $user->getId(), + userId: $user->getInternalId(), // Pass first, most verbose event pattern event: $event, resource: $resource, @@ -49,6 +48,7 @@ class AuditsV1 extends Worker ip: $ip, location: '', data: [ + 'userId' => $user->getId(), 'userName' => $userName, 'userEmail' => $userEmail, 'mode' => $mode, From 187f3dc6ff85a4c4a2239da9fe5e6e4c2f58ab53 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 12 Jul 2023 10:27:57 -0700 Subject: [PATCH 34/55] Update get logs APIs to return userId from data The audit userId is actually the internal id and the user id is in the data attribute. --- app/controllers/api/databases.php | 6 +++--- app/controllers/api/teams.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 4459322baf..29f9c6d0be 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -566,7 +566,7 @@ App::get('/v1/databases/:databaseId/logs') $output[$i] = new Document([ 'event' => $log['event'], - 'userId' => ID::custom($log['userId']), + 'userId' => ID::custom($log['data']['userId']), 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, @@ -917,7 +917,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs') $output[$i] = new Document([ 'event' => $log['event'], - 'userId' => $log['userId'], + 'userId' => $log['data']['userId'], 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, @@ -3149,7 +3149,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen $output[$i] = new Document([ 'event' => $log['event'], - 'userId' => $log['userId'], + 'userId' => $log['data']['userId'], 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index cdac9ba2fb..a06ab6b2a0 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1043,7 +1043,7 @@ App::get('/v1/teams/:teamId/logs') $output[$i] = new Document([ 'event' => $log['event'], - 'userId' => $log['userId'], + 'userId' => $log['data']['userId'], 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, From 9908a9021f6504d6d74ee5a8ea0379270ac6ac8e Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 12 Jul 2023 10:56:24 -0700 Subject: [PATCH 35/55] Update get logs by user to pass user internal id The userId in audit is actually the userInternalId. --- app/controllers/api/account.php | 2 +- app/controllers/api/users.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 3cd2c7bd70..37485b92d8 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1427,7 +1427,7 @@ App::get('/v1/account/logs') $audit = new EventAudit($dbForProject); - $logs = $audit->getLogsByUser($user->getId(), $limit, $offset); + $logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset); $output = []; diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index cd217afe31..d84d83ff77 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -577,7 +577,7 @@ App::get('/v1/users/:userId/logs') $audit = new Audit($dbForProject); - $logs = $audit->getLogsByUser($user->getId(), $limit, $offset); + $logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset); $output = []; From cd78706944aff2372d5db684b84241a6a1a84d52 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 12 Jul 2023 10:57:55 -0700 Subject: [PATCH 36/55] Update migration for audit collection 1. Remove the userInternalId attribute 2. Replace userId in audit documents with userInternalId and put userId in data --- src/Appwrite/Migration/Version/V18.php | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/Appwrite/Migration/Version/V18.php b/src/Appwrite/Migration/Version/V18.php index be628e0fbb..839269f940 100644 --- a/src/Appwrite/Migration/Version/V18.php +++ b/src/Appwrite/Migration/Version/V18.php @@ -133,6 +133,16 @@ class V18 extends Migration Console::warning("'options' from {$id}: {$th->getMessage()}"); } break; + case 'audit': + try { + /** + * Delete 'userInternalId' attribute + */ + $this->projectDB->deleteAttribute($id, 'userInternalId'); + } catch (\Throwable $th) { + Console::warning("'userInternalId' from {$id}: {$th->getMessage()}"); + } + break; default: break; } @@ -195,6 +205,34 @@ class V18 extends Migration Console::warning($th->getMessage()); } break; + case 'audit': + /** + * Set the userId to the userInternalId and add userId to data + */ + try { + $userId = $document->getAttribute('userId'); + $data = $document->getAttribute('data', []); + $mode = $data['mode'] ?? 'default'; + $user = match ($mode) { + 'admin' => $this->consoleDB->getDocument('users', $userId), + default => $this->projectDB->getDocument('users', $userId), + }; + + if ($user->isEmpty()) { + // The audit userId could already be an internal Id. + // Otherwise, the user could have been deleted. + // Nonetheless, there's nothing else we can do here. + break; + } + $internalId = $user->getInternalId(); + $document->setAttribute('userId', $internalId); + $data = $document->getAttribute('data', []); + $data['userId'] = $user->getId(); + $document->setAttribute('data', $data); + } catch (\Throwable $th) { + Console::warning($th->getMessage()); + } + break; } return $document; From 3f9cedcc44e766f25c0a1f91f4c70d56bb81d0bf Mon Sep 17 00:00:00 2001 From: Steven <1477010+stnguyen90@users.noreply.github.com> Date: Tue, 18 Jul 2023 22:21:58 +0000 Subject: [PATCH 37/55] Prepare 1.3.8 release --- CHANGES.md | 5 +++++ README-CN.md | 6 +++--- README.md | 6 +++--- app/init.php | 2 +- src/Appwrite/Migration/Migration.php | 1 + 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d19c8490b7..fb46b2b7d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,8 @@ +# Version 1.3.8 + +## Bugs +- Fix audit user internal [#5809](https://github.com/appwrite/appwrite/pull/5809) + # Version 1.3.7 ## Bugs diff --git a/README-CN.md b/README-CN.md index d3f52058b5..b634458547 100644 --- a/README-CN.md +++ b/README-CN.md @@ -66,7 +66,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:1.3.7 + appwrite/appwrite:1.3.8 ``` ### Windows @@ -78,7 +78,7 @@ docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --entrypoint="install" ^ - appwrite/appwrite:1.3.7 + appwrite/appwrite:1.3.8 ``` #### PowerShell @@ -88,7 +88,7 @@ docker run -it --rm ` --volume /var/run/docker.sock:/var/run/docker.sock ` --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ` --entrypoint="install" ` - appwrite/appwrite:1.3.7 + appwrite/appwrite:1.3.8 ``` 运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。 diff --git a/README.md b/README.md index b8fffd110a..5099cd6238 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:1.3.7 + appwrite/appwrite:1.3.8 ``` ### Windows @@ -87,7 +87,7 @@ docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --entrypoint="install" ^ - appwrite/appwrite:1.3.7 + appwrite/appwrite:1.3.8 ``` #### PowerShell @@ -97,7 +97,7 @@ docker run -it --rm ` --volume /var/run/docker.sock:/var/run/docker.sock ` --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ` --entrypoint="install" ` - appwrite/appwrite:1.3.7 + appwrite/appwrite:1.3.8 ``` Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation. diff --git a/app/init.php b/app/init.php index fc7541513d..132acb62ce 100644 --- a/app/init.php +++ b/app/init.php @@ -101,7 +101,7 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours const APP_CACHE_BUSTER = 506; -const APP_VERSION_STABLE = '1.3.7'; +const APP_VERSION_STABLE = '1.3.8'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index 7a1f3cad24..ffb5ce6ac4 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -63,6 +63,7 @@ abstract class Migration '1.3.5' => 'V18', '1.3.6' => 'V18', '1.3.7' => 'V18', + '1.3.8' => 'V18', ]; /** From 1e0158cdef7658ad6f83de91df2623256d58e230 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 20 Jul 2023 11:35:59 +0545 Subject: [PATCH 38/55] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index b8fffd110a..5613d4f560 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,12 @@ Choose from one of the providers below:
Gitpod + + + Akamai Logo +
Akamai
+ + From 2c2c7d9028eafe333d47c94294bca75762292891 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 20 Jul 2023 11:36:40 +0545 Subject: [PATCH 39/55] Create akamai-logo.svg --- public/images/integrations/akamai-logo.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 public/images/integrations/akamai-logo.svg diff --git a/public/images/integrations/akamai-logo.svg b/public/images/integrations/akamai-logo.svg new file mode 100644 index 0000000000..cdcf31b4c2 --- /dev/null +++ b/public/images/integrations/akamai-logo.svg @@ -0,0 +1,3 @@ + + + From b700336d45c64a386939db34cc8db93119290c12 Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Thu, 20 Jul 2023 14:52:05 +0530 Subject: [PATCH 40/55] fix get index route to use find --- app/controllers/api/databases.php | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 37694fcb65..d16bd57f4c 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2524,28 +2524,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { - $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); + $index = $dbForProject->find('indexes', [Query::equal('collectionId', [$collectionId]), Query::equal('key', [$key]), Query::equal('databaseId', [$databaseId]), Query::limit(1)]); - if ($database->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); - - if ($collection->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - $index = $dbForProject->findOne('indexes', [ - Query::equal('$id', [$database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key]) - ]); - - if ($index->isEmpty()) { + if (empty($index)) { throw new Exception(Exception::INDEX_NOT_FOUND); } - $index->setAttribute('collectionId', $database->getInternalId() . '_' . $collectionId); - - $response->dynamic($index, Response::MODEL_INDEX); + $response->dynamic($index[0], Response::MODEL_INDEX); }); ; From 5be6d8e90bd3fe275c31750169ab6471f94cfbfd Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Thu, 20 Jul 2023 15:54:38 -0700 Subject: [PATCH 41/55] Update Project providers to also include the provider key Pass the provider key and name back so that a nicely formatted name can be shown in the Appwrite Console. --- app/config/providers.php | 4 ++-- src/Appwrite/Utopia/Response/Model/Project.php | 5 +++-- src/Appwrite/Utopia/Response/Model/Provider.php | 6 ++++++ tests/e2e/Services/Projects/ProjectsConsoleClientTest.php | 4 ++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/config/providers.php b/app/config/providers.php index 5e4dc0aa11..dedb4ec665 100644 --- a/app/config/providers.php +++ b/app/config/providers.php @@ -232,7 +232,7 @@ return [ // Ordered by ABC. 'mock' => false ], 'paypalSandbox' => [ - 'name' => 'PayPal', + 'name' => 'PayPal Sandbox', 'developers' => 'https://developer.paypal.com/docs/api/overview/', 'icon' => 'icon-paypal', 'enabled' => true, @@ -302,7 +302,7 @@ return [ // Ordered by ABC. 'mock' => false, ], 'tradeshiftBox' => [ - 'name' => 'Tradeshift', + 'name' => 'Tradeshift Sandbox', 'developers' => 'https://developers.tradeshift.com/docs/api', 'icon' => 'icon-tradeshiftbox', 'enabled' => true, diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 6fbc794698..ed41c250fc 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -136,7 +136,7 @@ class Project extends Model 'type' => Response::MODEL_PROVIDER, 'description' => 'List of Providers.', 'default' => [], - 'example' => new \stdClass(), + 'example' => [new \stdClass()], 'array' => true, ]) ->addRule('platforms', [ @@ -273,7 +273,8 @@ class Project extends Model } $projectProviders[] = new Document([ - 'name' => ucfirst($key), + 'key' => $key, + 'name' => $provider['name'] ?? '', 'appId' => $providerValues[$key . 'Appid'] ?? '', 'secret' => $providerValues[$key . 'Secret'] ?? '', 'enabled' => $providerValues[$key . 'Enabled'] ?? false, diff --git a/src/Appwrite/Utopia/Response/Model/Provider.php b/src/Appwrite/Utopia/Response/Model/Provider.php index 0f14993508..c589011a46 100644 --- a/src/Appwrite/Utopia/Response/Model/Provider.php +++ b/src/Appwrite/Utopia/Response/Model/Provider.php @@ -15,6 +15,12 @@ class Provider extends Model public function __construct() { $this + ->addRule('key', [ + 'type' => self::TYPE_STRING, + 'description' => 'Provider.', + 'default' => '', + 'example' => 'github', + ]) ->addRule('name', [ 'type' => self::TYPE_STRING, 'description' => 'Provider name.', diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index c09bd262e3..52991b4621 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -618,7 +618,7 @@ class ProjectsConsoleClientTest extends Scope foreach ($providers as $key => $provider) { $asserted = false; foreach ($response['body']['providers'] as $responseProvider) { - if ($responseProvider['name'] === ucfirst($key)) { + if ($responseProvider['key'] === $key) { $this->assertEquals('AppId-' . ucfirst($key), $responseProvider['appId']); $this->assertEquals('Secret-' . ucfirst($key), $responseProvider['secret']); $this->assertFalse($responseProvider['enabled']); @@ -660,7 +660,7 @@ class ProjectsConsoleClientTest extends Scope foreach ($providers as $key => $provider) { $asserted = false; foreach ($response['body']['providers'] as $responseProvider) { - if ($responseProvider['name'] === ucfirst($key)) { + if ($responseProvider['key'] === $key) { // On first provider, test enabled=false $this->assertEquals($i !== 0, $responseProvider['enabled']); $asserted = true; From 7d412b4741899addb72f73fd34098609f023853b Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Mon, 24 Jul 2023 12:02:28 +0530 Subject: [PATCH 42/55] adds get db call and get collection call --- app/controllers/api/databases.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 6bfc08731f..4f2f42c849 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2524,7 +2524,18 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { - $index = $dbForProject->find('indexes', [Query::equal('collectionId', [$collectionId]), Query::equal('key', [$key]), Query::equal('databaseId', [$databaseId]), Query::limit(1)]); + $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); + + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); + + if ($collection->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $index = $dbForProject->find('indexes', [Query::equal('$id', [$database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key]), Query::limit(1)]); if (empty($index)) { throw new Exception(Exception::INDEX_NOT_FOUND); @@ -2532,7 +2543,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') $response->dynamic($index[0], Response::MODEL_INDEX); }); -; + App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) From f7c02d9308039aed8c540c31dc9e68fd4bbc0ae3 Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Mon, 24 Jul 2023 23:41:49 +0530 Subject: [PATCH 43/55] removes extra db call and uses document find method --- app/controllers/api/databases.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 4f2f42c849..c815954747 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2535,13 +2535,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $index = $dbForProject->find('indexes', [Query::equal('$id', [$database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key]), Query::limit(1)]); - + $index = $collection->find('key', $key, 'indexes'); if (empty($index)) { throw new Exception(Exception::INDEX_NOT_FOUND); } - $response->dynamic($index[0], Response::MODEL_INDEX); + $response->dynamic($index, Response::MODEL_INDEX); }); From 3b4255c457c1cf150f32bfd003cc3e2b7f844811 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 1 Aug 2023 18:18:27 +0530 Subject: [PATCH 44/55] fix: error in initialising the Exception constructor in api.php --- app/controllers/shared/api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 289fbca8f4..2f0bbbf153 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -27,7 +27,7 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar $parts = explode('.', $match); if (count($parts) !== 2) { - throw new Exception('Too less or too many parts', 400, Exception::GENERAL_ARGUMENT_INVALID); + throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label"); } $namespace = $parts[0] ?? ''; From f6d2df8115dd11648000909b3759c6ed863fb4be Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 1 Aug 2023 12:21:34 -0400 Subject: [PATCH 45/55] Update app/controllers/shared/api.php Co-authored-by: Christy Jacob --- app/controllers/shared/api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 2f0bbbf153..daa7f9d5da 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -27,7 +27,7 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar $parts = explode('.', $match); if (count($parts) !== 2) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label"); + throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label. Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose"); } $namespace = $parts[0] ?? ''; From 0fe59d457b08fbbf588a128e8a410e618d48308e Mon Sep 17 00:00:00 2001 From: Safwan Parkar Date: Tue, 1 Aug 2023 22:57:59 +0400 Subject: [PATCH 46/55] fixed stale team memberships on user --- app/controllers/api/teams.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index a06ab6b2a0..fe58129f1a 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -343,11 +343,17 @@ App::delete('/v1/teams/:teamId') Query::limit(2000), // TODO fix members limit ]); - // TODO delete all members individually from the user object foreach ($memberships as $membership) { if (!$dbForProject->deleteDocument('memberships', $membership->getId())) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove membership for team from DB'); } + + $user = $dbForProject->getDocument('users', $membership->getAttribute('userId')); + $user->setAttribute('memberships', array_values(array_filter( + $user->getAttribute('memberships', []), + fn($um) => $um['teamId'] !== $membership->getAttribute('teamId')) + )); + $dbForProject->updateDocument('users', $user->getId(), $user); } if (!$dbForProject->deleteDocument('teams', $teamId)) { From a45c62ab24c8a9e415a46290e968a70a3741bc12 Mon Sep 17 00:00:00 2001 From: Safwan Parkar Date: Tue, 1 Aug 2023 23:24:46 +0400 Subject: [PATCH 47/55] run composer scripts --- app/controllers/api/teams.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index fe58129f1a..f34187ea9a 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -350,9 +350,9 @@ App::delete('/v1/teams/:teamId') $user = $dbForProject->getDocument('users', $membership->getAttribute('userId')); $user->setAttribute('memberships', array_values(array_filter( - $user->getAttribute('memberships', []), - fn($um) => $um['teamId'] !== $membership->getAttribute('teamId')) - )); + $user->getAttribute('memberships', []), + fn($um) => $um['teamId'] !== $membership->getAttribute('teamId') + ))); $dbForProject->updateDocument('users', $user->getId(), $user); } From c5233d9ece9f9c0a82c6121a367d3f63ce032dd2 Mon Sep 17 00:00:00 2001 From: Safwan Parkar Date: Wed, 2 Aug 2023 12:18:21 +0400 Subject: [PATCH 48/55] removed unnecessary code --- app/controllers/api/teams.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index f34187ea9a..2a2611cc5c 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -347,13 +347,6 @@ App::delete('/v1/teams/:teamId') if (!$dbForProject->deleteDocument('memberships', $membership->getId())) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove membership for team from DB'); } - - $user = $dbForProject->getDocument('users', $membership->getAttribute('userId')); - $user->setAttribute('memberships', array_values(array_filter( - $user->getAttribute('memberships', []), - fn($um) => $um['teamId'] !== $membership->getAttribute('teamId') - ))); - $dbForProject->updateDocument('users', $user->getId(), $user); } if (!$dbForProject->deleteDocument('teams', $teamId)) { From 2bc2061f091eb4e3ba358292057dac1db02c4273 Mon Sep 17 00:00:00 2001 From: Safwan Parkar Date: Wed, 2 Aug 2023 18:44:43 +0400 Subject: [PATCH 49/55] fix showing of stale team memberships --- app/controllers/api/users.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index d84d83ff77..fa92739510 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -530,6 +530,10 @@ App::get('/v1/users/:userId/memberships') $memberships = array_map(function ($membership) use ($dbForProject, $user) { $team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId')); + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + $membership ->setAttribute('teamName', $team->getAttribute('name')) ->setAttribute('userName', $user->getAttribute('name')) From a14d89eb3cc3cc7fb28d154682b33565f990d0b1 Mon Sep 17 00:00:00 2001 From: Safwan Parkar Date: Thu, 3 Aug 2023 21:39:18 +0400 Subject: [PATCH 50/55] remove error-causing condition --- app/controllers/api/users.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index fa92739510..d84d83ff77 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -530,10 +530,6 @@ App::get('/v1/users/:userId/memberships') $memberships = array_map(function ($membership) use ($dbForProject, $user) { $team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId')); - if ($team->isEmpty()) { - throw new Exception(Exception::TEAM_NOT_FOUND); - } - $membership ->setAttribute('teamName', $team->getAttribute('name')) ->setAttribute('userName', $user->getAttribute('name')) From d142620c5e9ec7f3125e6d39f0c4c7d302bfc62a Mon Sep 17 00:00:00 2001 From: Safwan Parkar Date: Thu, 3 Aug 2023 22:08:27 +0400 Subject: [PATCH 51/55] invalidate cached document of user - cache caused stale data in memberships --- app/controllers/api/teams.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 2a2611cc5c..2b2f3b6920 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -344,9 +344,11 @@ App::delete('/v1/teams/:teamId') ]); foreach ($memberships as $membership) { + // Memberships are deleted here instead of in the worker to make sure user permisions are updated instantly if (!$dbForProject->deleteDocument('memberships', $membership->getId())) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove membership for team from DB'); } + $dbForProject->deleteCachedDocument('users', $membership->getAttribute('userId')); } if (!$dbForProject->deleteDocument('teams', $teamId)) { From 06e1191063435bb184a356aa571827301554f623 Mon Sep 17 00:00:00 2001 From: Safwan Parkar Date: Fri, 4 Aug 2023 00:34:01 +0400 Subject: [PATCH 52/55] added test --- tests/e2e/Services/Teams/TeamsBase.php | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/e2e/Services/Teams/TeamsBase.php b/tests/e2e/Services/Teams/TeamsBase.php index 8e19bede17..7d1e51c08c 100644 --- a/tests/e2e/Services/Teams/TeamsBase.php +++ b/tests/e2e/Services/Teams/TeamsBase.php @@ -428,4 +428,67 @@ trait TeamsBase $this->assertEquals($user['headers']['status-code'], 400); } + + public function testTeamDeleteUpdatesUserMembership() + { + $users = []; + $team = null; + + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => ID::unique(), + 'name' => 'Demo' + ]); + + $this->assertEquals(201, $team['headers']['status-code']); + $this->assertNotEmpty($team['body']['$id']); + $this->assertEquals('Demo', $team['body']['name']); + $this->assertGreaterThan(-1, $team['body']['total']); + $this->assertIsInt($team['body']['total']); + + for ($i = 0; $i < 5; $i++) { + $mem = $this->client->call(Client::METHOD_POST, '/teams/' . $team['body']['$id'] . '/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'email' => 'email' . $i . '@example.com', + 'roles' => ['admin', 'editor'], + 'name' => 'User ' . $i, + 'url' => 'http://localhost:5000/join-us#title' + ]); + + $this->assertEquals(201, $mem['headers']['status-code']); + $this->assertNotEmpty($mem['body']['$id']); + $this->assertNotEmpty($mem['body']['userId']); + $this->assertEquals('User ' . $i, $mem['body']['userName']); + $this->assertEquals('email' . $i . '@example.com', $mem['body']['userEmail']); + $this->assertNotEmpty($mem['body']['teamId']); + $this->assertCount(2, $mem['body']['roles']); + } + + $this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + foreach ($users as $user) { + $user = $this->client->call(Client::METHOD_GET, '/users/' . $user['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $user['headers']['status-code']); + $this->assertEquals(0, $user['body']['total']); + $this->assertEquals([], $user['body']['memberships']); + } + + $team = $this->client->call(Client::METHOD_GET, '/teams/' . $team['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $team['headers']['status-code']); + } } From 451f4bee19d31a1408049885a2f8fe6d3fc3faa5 Mon Sep 17 00:00:00 2001 From: Safwan Parkar Date: Fri, 4 Aug 2023 15:40:42 +0400 Subject: [PATCH 53/55] Fixed an incorrect test for team deletion This commit contains changes in 3 places. - First I changed the placement of an informative comment in the DELETE TEAM controller, and moved it outside of the loop. I did this when Steven pointed out that the behaviour I describe in the comment is for the whole loop. - The second change is the removal of my first test. I was facing quite a few issues with creating users in the test, and ended up using the CREATE TEAM MEMBERSHIP to perform 2 actions at once -> create a new user if one doesn't exist with the provided email, and create a membership for the user. Before this approach, I had quite a bit of code that didn't work, and it seems like I removed some things that weren't supposed to be removed, and didn't change variable names where necessary. Anyway, I figured that the problem has something to do with the user being created on the client side, so I moved the test to the server side. - The new test I implemented does the same thing as my previous failed test, but in more detailed and distinct steps. The test first creates 5 new users inside of a loop, and pushes each new user's ID to an array called 'new_users' if the response is as expected. Then a new team is created. The next step is to create memberships for all 5 users. If all these steps pass, the new team that was just created, is deleted, and we check to make sure the new users have 0 team memberships each. Formatter and linter showed no errors. Tests were successful on localhost. --- app/controllers/api/teams.php | 2 +- tests/e2e/Services/Teams/TeamsBase.php | 63 ------------- tests/e2e/Services/Teams/TeamsBaseServer.php | 98 ++++++++++++++++++++ 3 files changed, 99 insertions(+), 64 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 2b2f3b6920..5c5c128f57 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -343,8 +343,8 @@ App::delete('/v1/teams/:teamId') Query::limit(2000), // TODO fix members limit ]); + // Memberships are deleted here instead of in the worker to make sure user permisions are updated instantly foreach ($memberships as $membership) { - // Memberships are deleted here instead of in the worker to make sure user permisions are updated instantly if (!$dbForProject->deleteDocument('memberships', $membership->getId())) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove membership for team from DB'); } diff --git a/tests/e2e/Services/Teams/TeamsBase.php b/tests/e2e/Services/Teams/TeamsBase.php index 7d1e51c08c..8e19bede17 100644 --- a/tests/e2e/Services/Teams/TeamsBase.php +++ b/tests/e2e/Services/Teams/TeamsBase.php @@ -428,67 +428,4 @@ trait TeamsBase $this->assertEquals($user['headers']['status-code'], 400); } - - public function testTeamDeleteUpdatesUserMembership() - { - $users = []; - $team = null; - - $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'teamId' => ID::unique(), - 'name' => 'Demo' - ]); - - $this->assertEquals(201, $team['headers']['status-code']); - $this->assertNotEmpty($team['body']['$id']); - $this->assertEquals('Demo', $team['body']['name']); - $this->assertGreaterThan(-1, $team['body']['total']); - $this->assertIsInt($team['body']['total']); - - for ($i = 0; $i < 5; $i++) { - $mem = $this->client->call(Client::METHOD_POST, '/teams/' . $team['body']['$id'] . '/memberships', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'email' => 'email' . $i . '@example.com', - 'roles' => ['admin', 'editor'], - 'name' => 'User ' . $i, - 'url' => 'http://localhost:5000/join-us#title' - ]); - - $this->assertEquals(201, $mem['headers']['status-code']); - $this->assertNotEmpty($mem['body']['$id']); - $this->assertNotEmpty($mem['body']['userId']); - $this->assertEquals('User ' . $i, $mem['body']['userName']); - $this->assertEquals('email' . $i . '@example.com', $mem['body']['userEmail']); - $this->assertNotEmpty($mem['body']['teamId']); - $this->assertCount(2, $mem['body']['roles']); - } - - $this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - foreach ($users as $user) { - $user = $this->client->call(Client::METHOD_GET, '/users/' . $user['body']['$id'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(200, $user['headers']['status-code']); - $this->assertEquals(0, $user['body']['total']); - $this->assertEquals([], $user['body']['memberships']); - } - - $team = $this->client->call(Client::METHOD_GET, '/teams/' . $team['body']['$id'], array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(404, $team['headers']['status-code']); - } } diff --git a/tests/e2e/Services/Teams/TeamsBaseServer.php b/tests/e2e/Services/Teams/TeamsBaseServer.php index aa1be49f41..569170f890 100644 --- a/tests/e2e/Services/Teams/TeamsBaseServer.php +++ b/tests/e2e/Services/Teams/TeamsBaseServer.php @@ -4,6 +4,7 @@ namespace Tests\E2E\Services\Teams; use Tests\E2E\Client; use Utopia\Database\Validator\Datetime as DatetimeValidator; +use Utopia\Database\Helpers\ID; trait TeamsBaseServer { @@ -281,4 +282,101 @@ trait TeamsBaseServer $this->assertIsInt($response['body']['total']); $this->assertEquals(true, $dateValidator->isValid($response['body']['$createdAt'])); } + + public function testTeamDeleteUpdatesUserMembership() + { + $new_users = []; + + /** + * Create 5 new users and add their IDs to an array + */ + for ($i = 0; $i < 5; $i++) { + $user = $this->client->call(Client::METHOD_POST, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'userId' => ID::unique(), + 'email' => 'newuser' . $i . '@localhost.test', + 'password' => 'password', + 'name' => 'New User ' . $i, + ], false); + + $user_body = json_decode($user['body'], true); + + $this->assertEquals(201, $user['headers']['status-code']); + $this->assertEquals('newuser' . $i . '@localhost.test', $user_body['email']); + $this->assertEquals('New User ' . $i, $user_body['name']); + $this->assertEquals($user_body['status'], true); + + array_push($new_users, $user_body['$id']); + } + + /** + * Create a new team + */ + $new_team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => ID::unique(), + 'name' => 'New Team Test', + 'roles' => ['admin', 'editor'], + ]); + + $this->assertEquals(201, $new_team['headers']['status-code']); + $this->assertNotEmpty($new_team['body']['$id']); + $this->assertEquals('New Team Test', $new_team['body']['name']); + $this->assertGreaterThan(-1, $new_team['body']['total']); + $this->assertIsInt($new_team['body']['total']); + $this->assertArrayHasKey('prefs', $new_team['body']); + + /** + * Create team memberships for each of the new users + */ + for ($i = 0; $i < 5; $i++) { + $new_membership = $this->client->call(Client::METHOD_POST, '/teams/' . $new_team['body']['$id'] . '/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'email' => 'newuser' . $i . '@localhost.test', + 'name' => 'New User ' . $i, + 'roles' => ['admin', 'editor'], + 'url' => 'http://localhost:5000/join-us#title' + ]); + + $this->assertEquals(201, $new_membership['headers']['status-code']); + $this->assertNotEmpty($new_membership['body']['$id']); + $this->assertNotEmpty($new_membership['body']['userId']); + $this->assertEquals('New User ' . $i, $new_membership['body']['userName']); + $this->assertEquals('newuser' . $i . '@localhost.test', $new_membership['body']['userEmail']); + $this->assertNotEmpty($new_membership['body']['teamId']); + $this->assertCount(2, $new_membership['body']['roles']); + $dateValidator = new DatetimeValidator(); + $this->assertEquals(true, $dateValidator->isValid($new_membership['body']['joined'])); + $this->assertEquals(true, $new_membership['body']['confirm']); + } + + /** + * Delete the team + */ + $team_del_response = $this->client->call(Client::METHOD_DELETE, '/teams/' . $new_team['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $team_del_response['headers']['status-code']); + + /** + * Check that the team memberships for each of the new users has been deleted + */ + for ($i = 0; $i < 5; $i++) { + $membership = $this->client->call(Client::METHOD_GET, '/users/' . $new_users[$i] . '/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $membership['headers']['status-code']); + $this->assertEquals(0, $membership['body']['total']); + } + } } From 0295c6ec1b2919385e516d4b86441fee5ff8e9df Mon Sep 17 00:00:00 2001 From: Safwan Parkar Date: Fri, 4 Aug 2023 23:17:41 +0400 Subject: [PATCH 54/55] improve test by removing user creation loop The CREATE TEAM MEMBERSHIP endpoint requires the email of the user to be added to the team. If the user does not exist in the project, a new user is created with the specified email and added to the team. The first version of the test creates 5 users, and then adds them to the newly created team. This process is more streamlined now, by using the CREATE TEAM MEMBERSHIPS behaviour to create a user on the go and create a membership for them immediately after. I also change the way I add user IDs to the array, by using the shorthand notation instead of the `array_push` function. --- tests/e2e/Services/Teams/TeamsBaseServer.php | 30 ++++---------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/tests/e2e/Services/Teams/TeamsBaseServer.php b/tests/e2e/Services/Teams/TeamsBaseServer.php index 569170f890..88bee1d62b 100644 --- a/tests/e2e/Services/Teams/TeamsBaseServer.php +++ b/tests/e2e/Services/Teams/TeamsBaseServer.php @@ -285,32 +285,9 @@ trait TeamsBaseServer public function testTeamDeleteUpdatesUserMembership() { + // Array to store the IDs of newly created users $new_users = []; - /** - * Create 5 new users and add their IDs to an array - */ - for ($i = 0; $i < 5; $i++) { - $user = $this->client->call(Client::METHOD_POST, '/users', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'userId' => ID::unique(), - 'email' => 'newuser' . $i . '@localhost.test', - 'password' => 'password', - 'name' => 'New User ' . $i, - ], false); - - $user_body = json_decode($user['body'], true); - - $this->assertEquals(201, $user['headers']['status-code']); - $this->assertEquals('newuser' . $i . '@localhost.test', $user_body['email']); - $this->assertEquals('New User ' . $i, $user_body['name']); - $this->assertEquals($user_body['status'], true); - - array_push($new_users, $user_body['$id']); - } - /** * Create a new team */ @@ -331,7 +308,8 @@ trait TeamsBaseServer $this->assertArrayHasKey('prefs', $new_team['body']); /** - * Create team memberships for each of the new users + * Use the Create Team Membership endpoint + * to create 5 new users and add them to the team immediately */ for ($i = 0; $i < 5; $i++) { $new_membership = $this->client->call(Client::METHOD_POST, '/teams/' . $new_team['body']['$id'] . '/memberships', array_merge([ @@ -354,6 +332,8 @@ trait TeamsBaseServer $dateValidator = new DatetimeValidator(); $this->assertEquals(true, $dateValidator->isValid($new_membership['body']['joined'])); $this->assertEquals(true, $new_membership['body']['confirm']); + + $new_users[] = $new_membership['body']['userId']; } /** From 4a9af13f167e938da417bf2a1eedf6cb39215e99 Mon Sep 17 00:00:00 2001 From: Safwan Parkar Date: Fri, 4 Aug 2023 23:41:29 +0400 Subject: [PATCH 55/55] run formatter and linter Run the composer format and lint commands, which I forgot to run before. --- tests/e2e/Services/Teams/TeamsBaseServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Teams/TeamsBaseServer.php b/tests/e2e/Services/Teams/TeamsBaseServer.php index 88bee1d62b..5950824da3 100644 --- a/tests/e2e/Services/Teams/TeamsBaseServer.php +++ b/tests/e2e/Services/Teams/TeamsBaseServer.php @@ -308,7 +308,7 @@ trait TeamsBaseServer $this->assertArrayHasKey('prefs', $new_team['body']); /** - * Use the Create Team Membership endpoint + * Use the Create Team Membership endpoint * to create 5 new users and add them to the team immediately */ for ($i = 0; $i < 5; $i++) {