From ec6e4f3a063f5cc4cd7b19132ef7cdbea6b71e36 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 14:46:16 +0100 Subject: [PATCH 01/65] adapt user collection properties --- app/config/collections.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index 9170b0757..3307e7a12 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -204,7 +204,7 @@ $collections = [ 'key' => 'email', 'type' => Database::SYSTEM_VAR_TYPE_EMAIL, 'default' => '', - 'required' => true, + 'required' => false, 'array' => false, ], [ @@ -222,7 +222,7 @@ $collections = [ 'key' => 'password', 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', - 'required' => true, + 'required' => false, 'array' => false, ], [ From 9d189165040fe956ef4aaa9dfcb119aa95faae39 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 14:46:30 +0100 Subject: [PATCH 02/65] create new endpoint --- app/controllers/api/account.php | 141 ++++++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index e8ae76312..bae62db4a 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -564,6 +564,128 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ; }); +App::post('/v1/account/sessions/anonymous') + ->desc('Create Anonymous Account') + ->groups(['api', 'account']) + ->label('event', 'account.sessions.create') + ->label('scope', 'public') + ->label('sdk.platform', [APP_PLATFORM_CLIENT]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'createAnonymous') + ->label('sdk.description', '/docs/references/account/create-anonymous.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_SESSION) + ->label('abuse-limit', 50) + ->label('abuse-key', 'ip:{ip}') + ->inject('request') + ->inject('response') + ->inject('locale') + ->inject('user') + ->inject('projectDB') + ->inject('geodb') + ->inject('audits') + ->action(function ($request, $response, $locale, $user, $projectDB, $geodb, $audits) { + /** @var Utopia\Swoole\Request $request */ + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Locale\Locale $locale */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var MaxMind\Db\Reader $geodb */ + /** @var Appwrite\Event\Event $audits */ + + $protocol = $request->getProtocol(); + + if ($user->getId()) { + throw new Exception('Failed to create anonymous user.', 401); + } + + Authorization::disable(); + try { + $user = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => [ + 'read' => ['*'], + 'write' => ['user:{self}'] + ], + 'email' => null, + 'emailVerification' => false, + 'status' => Auth::USER_STATUS_UNACTIVATED, + 'password' => null, + 'passwordUpdate' => \time(), + 'registration' => \time(), + 'reset' => false, + 'name' => null + ]); + } catch (Exception $th) { + throw new Exception('Failed saving user to DB', 500); + } + + Authorization::enable(); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + // Create session token, verify user account and update OAuth2 ID and Access Token + + $detector = new Detector($request->getUserAgent('UNKNOWN')); + $record = $geodb->get($request->getIP()); + $secret = Auth::tokenGenerator(); + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $session = new Document(array_merge( + [ + '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['user:' . $user['$id']], 'write' => ['user:' . $user['$id']]], + 'userId' => $user->getId(), + 'type' => Auth::TOKEN_TYPE_LOGIN, + 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak + 'expire' => $expiry, + 'userAgent' => $request->getUserAgent('UNKNOWN'), + 'ip' => $request->getIP(), + 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', + ], + $detector->getOS(), + $detector->getClient(), + $detector->getDevice() + )); + + $user->setAttribute('tokens', $session, Document::SET_TYPE_APPEND); + + Authorization::setRole('user:'.$user->getId()); + + $user = $projectDB->updateDocument($user->getArrayCopy()); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $audits + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.sessions.create') + ->setParam('resource', 'users/'.$user->getId()) + ; + + if (!Config::getParam('domainVerification')) { + $response + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) + ; + } + + $response + ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->setStatusCode(Response::STATUS_CODE_CREATED) + ; + + $session + ->setAttribute('current', true) + ->setAttribute('countryName', (isset($countries[$session->getAttribute('countryCode')])) ? $countries[$session->getAttribute('countryCode')] : $locale->getText('locale.country.unknown')) + ; + + $response->dynamic($session, Response::MODEL_SESSION); + }); + App::post('/v1/account/jwt') ->desc('Create Account JWT') ->groups(['api', 'account']) @@ -878,7 +1000,12 @@ App::patch('/v1/account/email') /** @var Appwrite\Database\Database $projectDB */ /** @var Appwrite\Event\Event $audits */ - if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password + $isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); // Check if request is from an anonymous account for converting + + if ( + !$isAnonymousUser && + !Auth::passwordVerify($password, $user->getAttribute('password')) + ) { // Double check user password throw new Exception('Invalid credentials', 401); } @@ -896,10 +1023,14 @@ App::patch('/v1/account/email') // TODO after this user needs to confirm mail again - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'email' => $email, - 'emailVerification' => false, - ])); + $user = $projectDB->updateDocument(\array_merge( + $user->getArrayCopy(), + ($isAnonymousUser ? [ 'password' => Auth::passwordHash($password) ] : []), + [ + 'email' => $email, + 'emailVerification' => false, + ] + )); if (false === $user) { throw new Exception('Failed saving user to DB', 500); From f199e70a8fc9552f0fc76b674d2b9e5432079adf Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 14:46:44 +0100 Subject: [PATCH 03/65] update docs --- docs/references/account/create-anonymous.md | 1 + docs/references/account/update-email.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 docs/references/account/create-anonymous.md diff --git a/docs/references/account/create-anonymous.md b/docs/references/account/create-anonymous.md new file mode 100644 index 000000000..61895604a --- /dev/null +++ b/docs/references/account/create-anonymous.md @@ -0,0 +1 @@ +Use this endpoint to allow a new user to register an anonymous account in your project. This route will also create a new session for the user. To allow the new user to convert an anonymous account to a normal account account, you need to update its [email and password](/docs/client/account#accountUpdateEmail). \ No newline at end of file diff --git a/docs/references/account/update-email.md b/docs/references/account/update-email.md index d99b9b4a1..7be36b7cd 100644 --- a/docs/references/account/update-email.md +++ b/docs/references/account/update-email.md @@ -1 +1,2 @@ -Update currently logged in user account email address. After changing user address, user confirmation status is being reset and a new confirmation mail is sent. For security measures, user password is required to complete this request. \ No newline at end of file +Update currently logged in user account email address. After changing user address, user confirmation status is being reset and a new confirmation mail is sent. For security measures, user password is required to complete this request. +This endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password. \ No newline at end of file From 6c717b7a314ac74ede2d69ebff0d43cb6d2379d2 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 14:49:21 +0100 Subject: [PATCH 04/65] add tests --- tests/e2e/Services/Account/AccountBase.php | 33 ++++++ .../Account/AccountCustomClientTest.php | 106 ++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index 5476e0455..b8aa1063a 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -49,6 +49,39 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 409); + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => '', + 'password' => '', + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => '', + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => '', + 'password' => $password, + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + return [ 'id' => $id, 'email' => $email, diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index a11165530..2a0eea743 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -226,4 +226,110 @@ class AccountCustomClientTest extends Scope return []; } + + public function testCreateAnonymousAccount() + { + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/anonymous', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals($response['headers']['status-code'], 201); + + $session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/anonymous', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]); + + $this->assertEquals($response['headers']['status-code'], 401); + + return $session; + } + + /** + * @depends testCreateAnonymousAccount + */ + public function testUpdateAccountPassword($session):array + { + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]), [ + 'password' => 'new-password', + 'oldPassword' => null, + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + return []; + } + + /** + * @depends testCreateAnonymousAccount + */ + public function testConvertAnonymousAccount($session):array + { + $email = uniqid().'new@localhost.test'; + $password = 'new-password'; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertIsArray($response['body']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertIsNumeric($response['body']['registration']); + $this->assertEquals($response['body']['email'], $email); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ])); + + $this->assertEquals($response['headers']['status-code'], 401); + + $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]), [ + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + return []; + } } \ No newline at end of file From 6005551023befdd556b8ca8056bf823a87ee31e8 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 15:14:36 +0100 Subject: [PATCH 05/65] update tests --- tests/e2e/Services/Account/AccountBase.php | 4 -- .../Account/AccountCustomClientTest.php | 53 ++++++++++--------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index b8aa1063a..38d985ab0 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -405,7 +405,6 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 200); $this->assertIsArray($response['body']); $this->assertNotEmpty($response['body']); - $this->assertNotEmpty($response['body']); $this->assertNotEmpty($response['body']['$id']); $this->assertIsNumeric($response['body']['registration']); $this->assertEquals($response['body']['email'], $email); @@ -473,7 +472,6 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 200); $this->assertIsArray($response['body']); $this->assertNotEmpty($response['body']); - $this->assertNotEmpty($response['body']); $this->assertNotEmpty($response['body']['$id']); $this->assertIsNumeric($response['body']['registration']); $this->assertEquals($response['body']['email'], $email); @@ -540,7 +538,6 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 200); $this->assertIsArray($response['body']); $this->assertNotEmpty($response['body']); - $this->assertNotEmpty($response['body']); $this->assertNotEmpty($response['body']['$id']); $this->assertIsNumeric($response['body']['registration']); $this->assertEquals($response['body']['email'], $newEmail); @@ -598,7 +595,6 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 200); $this->assertIsArray($response['body']); $this->assertNotEmpty($response['body']); - $this->assertNotEmpty($response['body']); $this->assertEquals('prefValue1', $response['body']['prefs']['prefKey1']); $this->assertEquals('prefValue2', $response['body']['prefs']['prefKey2']); diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 2a0eea743..498037d3e 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -260,10 +260,10 @@ class AccountCustomClientTest extends Scope /** * @depends testCreateAnonymousAccount */ - public function testUpdateAccountPassword($session):array + public function testUpdateAnonymousAccountPassword($session):array { /** - * Test for SUCCESS + * Test for FAILURE */ $response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([ 'origin' => 'http://localhost', @@ -272,7 +272,7 @@ class AccountCustomClientTest extends Scope 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, ]), [ 'password' => 'new-password', - 'oldPassword' => null, + 'oldPassword' => '', ]); $this->assertEquals($response['headers']['status-code'], 400); @@ -280,6 +280,31 @@ class AccountCustomClientTest extends Scope return []; } + /** + * @depends testUpdateAnonymousAccountPassword + */ + public function testUpdateAnonymousAccountEmail($session):array + { + $email = uniqid().'new@localhost.test'; + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]), [ + 'email' => $email, + 'password' => '', + ]); + + $this->assertEquals($response['headers']['status-code'], 401); + + return []; + } + /** * @depends testCreateAnonymousAccount */ @@ -304,32 +329,10 @@ class AccountCustomClientTest extends Scope $this->assertEquals($response['headers']['status-code'], 200); $this->assertIsArray($response['body']); $this->assertNotEmpty($response['body']); - $this->assertNotEmpty($response['body']); $this->assertNotEmpty($response['body']['$id']); $this->assertIsNumeric($response['body']['registration']); $this->assertEquals($response['body']['email'], $email); - /** - * Test for FAILURE - */ - $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); - - $this->assertEquals($response['headers']['status-code'], 401); - - $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, - ]), [ - ]); - - $this->assertEquals($response['headers']['status-code'], 400); - return []; } } \ No newline at end of file From c9b46d93dceba48739a76d2a4943769069c5f651 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 15:16:09 +0100 Subject: [PATCH 06/65] fix comment --- 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 bae62db4a..5ab3e6e15 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -627,7 +627,7 @@ App::post('/v1/account/sessions/anonymous') throw new Exception('Failed saving user to DB', 500); } - // Create session token, verify user account and update OAuth2 ID and Access Token + // Create session token $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); From b559f8068e5b33f5c52bd9f9ba362060ba572915 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 15:20:15 +0100 Subject: [PATCH 07/65] add test to login after converting --- .../e2e/Services/Account/AccountCustomClientTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 498037d3e..56dba168c 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -333,6 +333,17 @@ class AccountCustomClientTest extends Scope $this->assertIsNumeric($response['body']['registration']); $this->assertEquals($response['body']['email'], $email); + $response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals($response['headers']['status-code'], 201); + return []; } } \ No newline at end of file From 6a31e19d731727f9099de9af30cf6ed0e084a138 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 16:01:15 +0100 Subject: [PATCH 08/65] add test to convert to existing email --- .../Account/AccountCustomClientTest.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 56dba168c..73b79e2c8 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -313,9 +313,35 @@ class AccountCustomClientTest extends Scope $email = uniqid().'new@localhost.test'; $password = 'new-password'; + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => $password + ]); + + $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + /** * Test for SUCCESS */ + $email = uniqid().'new@localhost.test'; + $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', From 1387fb50f108818c65f49532a0bff95608c613cc Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 16:51:08 +0100 Subject: [PATCH 09/65] add tests for oauth2 --- app/controllers/api/account.php | 9 +++ .../Account/AccountCustomClientTest.php | 56 +++++++++++++++++-- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 5ab3e6e15..277f4e737 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -515,6 +515,15 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', ], $detector->getOS(), $detector->getClient(), $detector->getDevice())); + $isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); + + if ($isAnonymousUser) { + $user + ->setAttribute('name', $oauth2->getUserName($accessToken)) + ->setAttribute('email', $oauth2->getUserEmail($accessToken)) + ; + } + $user ->setAttribute('oauth2'.\ucfirst($provider), $oauth2ID) ->setAttribute('oauth2'.\ucfirst($provider).'AccessToken', $accessToken) diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 73b79e2c8..cf1af8749 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -305,11 +305,9 @@ class AccountCustomClientTest extends Scope return []; } - /** - * @depends testCreateAnonymousAccount - */ - public function testConvertAnonymousAccount($session):array + public function testConvertAnonymousAccount():array { + $session = $this->testCreateAnonymousAccount(); $email = uniqid().'new@localhost.test'; $password = 'new-password'; @@ -372,4 +370,54 @@ class AccountCustomClientTest extends Scope return []; } + + public function testConvertAnonymousAccountOAuth2():array + { + $session = $this->testCreateAnonymousAccount(); + $provider = 'mock'; + $appId = '1'; + $secret = '123456'; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/'.$this->getProject()['$id'].'/oauth2', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + ]), [ + 'provider' => $provider, + 'appId' => $appId, + 'secret' => $secret, + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + + $response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/'.$provider, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]), [ + 'success' => 'http://localhost/v1/mock/tests/general/oauth2/success', + 'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('success', $response['body']['result']); + + $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ])); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertEquals($response['body']['name'], 'User Name'); + $this->assertEquals($response['body']['email'], 'user@localhost.test'); + + return []; + } } \ No newline at end of file From d1ae8aa15c90342207e5cbbed4714b91c71a7081 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 17 Feb 2021 09:47:07 +0100 Subject: [PATCH 10/65] adapt to review --- app/controllers/api/account.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 277f4e737..5c095c231 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -109,7 +109,7 @@ App::post('/v1/account') throw new Exception('Account already exists', 409); } - Authorization::enable(); + Authorization::reset(); Authorization::unsetRole('role:'.Auth::USER_ROLE_GUEST); Authorization::setRole('user:'.$user->getId()); @@ -485,7 +485,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') throw new Exception('Account already exists', 409); } - Authorization::enable(); + Authorization::reset(); if (false === $user) { throw new Exception('Failed saving user to DB', 500); @@ -630,7 +630,7 @@ App::post('/v1/account/sessions/anonymous') throw new Exception('Failed saving user to DB', 500); } - Authorization::enable(); + Authorization::reset(); if (false === $user) { throw new Exception('Failed saving user to DB', 500); From 8d3d3bca681f88b23195875e0c2a2d7aa1f61b4f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 17 Feb 2021 10:38:45 +0100 Subject: [PATCH 11/65] added anon login to changelog --- CHANGES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 490007bcd..5b10ab43c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +# Version DEV (NOT RELEASED YET) + +## Features + +- Added Anonymous Login ([RFC-010](https://github.com/appwrite/rfc/blob/main/010-anonymous-login.md)) + # Version 0.7.0 ## Features From 2c1247d667b4b17fbc29c66254dd83ce712ff13d Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 18 Feb 2021 15:52:27 +0100 Subject: [PATCH 12/65] change sdk method name --- app/controllers/api/account.php | 6 +++--- .../{create-anonymous.md => create-session-anonymous.md} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename docs/references/account/{create-anonymous.md => create-session-anonymous.md} (100%) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 5c095c231..d90118d90 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -574,14 +574,14 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') }); App::post('/v1/account/sessions/anonymous') - ->desc('Create Anonymous Account') + ->desc('Create Anonymous Session') ->groups(['api', 'account']) ->label('event', 'account.sessions.create') ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') - ->label('sdk.method', 'createAnonymous') - ->label('sdk.description', '/docs/references/account/create-anonymous.md') + ->label('sdk.method', 'createAnonymousSession') + ->label('sdk.description', '/docs/references/account/create-session-anonymous.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_SESSION) diff --git a/docs/references/account/create-anonymous.md b/docs/references/account/create-session-anonymous.md similarity index 100% rename from docs/references/account/create-anonymous.md rename to docs/references/account/create-session-anonymous.md From e829f8f7bcbb478f311e61f71e393f96874f6d33 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 23 Feb 2021 23:43:05 +0100 Subject: [PATCH 13/65] add env var and console prevention --- .env | 1 + CHANGES.md | 1 + app/config/variables.php | 8 ++++++++ app/controllers/api/account.php | 10 ++++++++-- app/views/install/compose.phtml | 1 + docker-compose.yml | 1 + tests/e2e/Services/Account/AccountCustomClientTest.php | 5 +++-- 7 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 581af6d97..0f8c2e196 100644 --- a/.env +++ b/.env @@ -39,3 +39,4 @@ _APP_MAINTENANCE_RETENTION_EXECUTION=1209600 _APP_MAINTENANCE_RETENTION_ABUSE=86400 _APP_MAINTENANCE_RETENTION_AUDIT=1209600 _APP_USAGE_STATS=enabled +_APP_LOGIN_ANONYMOUS=enabled diff --git a/CHANGES.md b/CHANGES.md index 5b10ab43c..a822b68e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ ## Features - Added Anonymous Login ([RFC-010](https://github.com/appwrite/rfc/blob/main/010-anonymous-login.md)) +- Added new Environment Variable to enable or disable Anonymous Login # Version 0.7.0 diff --git a/app/config/variables.php b/app/config/variables.php index e842283c2..62d9d302a 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -119,6 +119,14 @@ return [ 'required' => false, 'question' => '', ], + [ + 'name' => '_APP_LOGIN_ANONYMOUS', + 'description' => 'This variable allows you to enable anonymous login.', + 'introduction' => '0.8.0', + 'default' => 'enabled', + 'required' => false, + 'question' => '', + ], ], ], [ diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index d90118d90..6bd6dcdd9 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -591,21 +591,27 @@ App::post('/v1/account/sessions/anonymous') ->inject('response') ->inject('locale') ->inject('user') + ->inject('project') ->inject('projectDB') ->inject('geodb') ->inject('audits') - ->action(function ($request, $response, $locale, $user, $projectDB, $geodb, $audits) { + ->action(function ($request, $response, $locale, $user, $project, $projectDB, $geodb, $audits) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Locale\Locale $locale */ /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Document $project */ /** @var Appwrite\Database\Database $projectDB */ /** @var MaxMind\Db\Reader $geodb */ /** @var Appwrite\Event\Event $audits */ $protocol = $request->getProtocol(); - if ($user->getId()) { + if(App::getEnv('_APP_LOGIN_ANONYMOUS', 'enabled') !== 'enabled') { + throw new Exception('Anonymous login is disabled.', 412); + } + + if ($user->getId() || 'console' === $project->getId()) { throw new Exception('Failed to create anonymous user.', 401); } diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index b98642fb9..daee965c5 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -89,6 +89,7 @@ services: - _APP_FUNCTIONS_MEMORY - _APP_FUNCTIONS_MEMORY_SWAP - _APP_FUNCTIONS_ENVS + - _APP_LOGIN_ANONYMOUS appwrite-worker-usage: image: appwrite/appwrite: diff --git a/docker-compose.yml b/docker-compose.yml index c682ac839..3b2260a20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -109,6 +109,7 @@ services: - _APP_FUNCTIONS_MEMORY - _APP_FUNCTIONS_MEMORY_SWAP - _APP_FUNCTIONS_ENVS + - _APP_LOGIN_ANONYMOUS appwrite-worker-usage: entrypoint: worker-usage diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index cf1af8749..e1dedd6be 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -6,6 +6,7 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\SideClient; +use Utopia\App; class AccountCustomClientTest extends Scope { @@ -238,7 +239,7 @@ class AccountCustomClientTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], ]); - $this->assertEquals($response['headers']['status-code'], 201); + $this->assertEquals(201, $response['headers']['status-code']); $session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; @@ -252,7 +253,7 @@ class AccountCustomClientTest extends Scope 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, ]); - $this->assertEquals($response['headers']['status-code'], 401); + $this->assertEquals(401, $response['headers']['status-code']); return $session; } From 59984fa6c04c97d708bb36ee30734f2f37ff5bc7 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Tue, 9 Mar 2021 14:58:03 -0500 Subject: [PATCH 14/65] Pass custom function $data to execution --- app/controllers/api/functions.php | 4 +++- app/workers/functions.php | 11 +++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 7a6b11bcd..d2d552016 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -672,11 +672,12 @@ App::post('/v1/functions/:functionId/executions') ->label('abuse-limit', 60) ->label('abuse-time', 60) ->param('functionId', '', new UID(), 'Function unique ID.') + ->param('data', '', new Text(8192), 'String of custom data to send to function.', true) // ->param('async', 1, new Range(0, 1), 'Execute code asynchronously. Pass 1 for true, 0 for false. Default value is 1.', true) ->inject('response') ->inject('project') ->inject('projectDB') - ->action(function ($functionId, /*$async,*/ $response, $project, $projectDB) { + ->action(function ($functionId, $data, /*$async,*/ $response, $project, $projectDB) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $project */ /** @var Appwrite\Database\Database $projectDB */ @@ -736,6 +737,7 @@ App::post('/v1/functions/:functionId/executions') 'functionId' => $function->getId(), 'executionId' => $execution->getId(), 'trigger' => 'http', + 'data' => $data, ]); $response diff --git a/app/workers/functions.php b/app/workers/functions.php index 87e3a8624..98eb126ff 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -148,6 +148,7 @@ class FunctionsV1 $event = $this->args['event'] ?? ''; $scheduleOriginal = $this->args['scheduleOriginal'] ?? ''; $payload = (!empty($this->args['payload'])) ? json_encode($this->args['payload']) : ''; + $data = $this->args['data'] ?? ''; $database = new Database(); $database->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); @@ -195,7 +196,7 @@ class FunctionsV1 Console::success('Triggered function: '.$event); - $this->execute('event', $projectId, '', $database, $function, $event, $payload); + $this->execute('event', $projectId, '', $database, $function, $event, $payload, $data); } } break; @@ -251,7 +252,7 @@ class FunctionsV1 'scheduleOriginal' => $function->getAttribute('schedule', ''), ]); // Async task rescheduale - $this->execute($trigger, $projectId, $executionId, $database, $function); + $this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$payload*/'', $data); break; @@ -264,7 +265,7 @@ class FunctionsV1 throw new Exception('Function not found ('.$functionId.')'); } - $this->execute($trigger, $projectId, $executionId, $database, $function); + $this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$payload*/'', $data); break; default: @@ -283,10 +284,11 @@ class FunctionsV1 * @param Database $function * @param string $event * @param string $payload + * @param string $data * * @return void */ - public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $payload = ''): void + public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $payload = '', string $data = ''): void { global $list; @@ -341,6 +343,7 @@ class FunctionsV1 'APPWRITE_FUNCTION_ENV_VERSION' => $environment['version'], 'APPWRITE_FUNCTION_EVENT' => $event, 'APPWRITE_FUNCTION_EVENT_PAYLOAD' => $payload, + 'APPWRITE_FUNCTION_DATA' => $data, ]); \array_walk($vars, function (&$value, $key) { From da8984f66e0389df7b966cfb794c666303855035 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 10 Mar 2021 11:58:46 -0500 Subject: [PATCH 15/65] Pass userId and JWT to function as env vars --- app/controllers/api/functions.php | 25 +++++++++++++++++++++++-- app/workers/functions.php | 12 ++++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index d2d552016..b3243ab93 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -677,10 +677,12 @@ App::post('/v1/functions/:functionId/executions') ->inject('response') ->inject('project') ->inject('projectDB') - ->action(function ($functionId, $data, /*$async,*/ $response, $project, $projectDB) { + ->inject('user') + ->action(function ($functionId, $data, /*$async,*/ $response, $project, $projectDB, $user) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $project */ /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Database\Document $user */ Authorization::disable(); @@ -731,13 +733,32 @@ App::post('/v1/functions/:functionId/executions') if (false === $execution) { throw new Exception('Failed saving execution to DB', 500); } - + + if (!empty($user->getId())) { // If userId exists, generate a JWT for function + + $tokens = $user->getAttribute('tokens', []); + $session = new Document(); + $jwt = ''; + + foreach ($tokens as $token) { /** @var Appwrite\Database\Document $token */ + if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + $session = $token; + } + } + + if(!$session->isEmpty()) { + $jwt = new JWT(App::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. + } + } + Resque::enqueue('v1-functions', 'FunctionsV1', [ 'projectId' => $project->getId(), 'functionId' => $function->getId(), 'executionId' => $execution->getId(), 'trigger' => 'http', 'data' => $data, + 'userId' => $user->getId(), + 'jwt' => $jwt, ]); $response diff --git a/app/workers/functions.php b/app/workers/functions.php index 98eb126ff..932c4cd58 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -149,6 +149,8 @@ class FunctionsV1 $scheduleOriginal = $this->args['scheduleOriginal'] ?? ''; $payload = (!empty($this->args['payload'])) ? json_encode($this->args['payload']) : ''; $data = $this->args['data'] ?? ''; + $userId = $this->args['userId'] ?? ''; + $jwt = $this->args['jwt'] ?? ''; $database = new Database(); $database->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); @@ -196,7 +198,7 @@ class FunctionsV1 Console::success('Triggered function: '.$event); - $this->execute('event', $projectId, '', $database, $function, $event, $payload, $data); + $this->execute('event', $projectId, '', $database, $function, $event, $payload, $data, $userId, $jwt); } } break; @@ -252,7 +254,7 @@ class FunctionsV1 'scheduleOriginal' => $function->getAttribute('schedule', ''), ]); // Async task rescheduale - $this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$payload*/'', $data); + $this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$payload*/'', $data, $userId, $jwt); break; @@ -265,7 +267,7 @@ class FunctionsV1 throw new Exception('Function not found ('.$functionId.')'); } - $this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$payload*/'', $data); + $this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$payload*/'', $data, $userId, $jwt); break; default: @@ -288,7 +290,7 @@ class FunctionsV1 * * @return void */ - public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $payload = '', string $data = ''): void + public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $payload = '', string $data = '', string $userId = '', string $jwt = ''): void { global $list; @@ -344,6 +346,8 @@ class FunctionsV1 'APPWRITE_FUNCTION_EVENT' => $event, 'APPWRITE_FUNCTION_EVENT_PAYLOAD' => $payload, 'APPWRITE_FUNCTION_DATA' => $data, + 'APPWRITE_FUNCTION_USERID' => $userId, + 'APPWRITE_FUNCTION_JWT' => $jwt, ]); \array_walk($vars, function (&$value, $key) { From a2bf6269bf2498a10f2871bb909ea4ea3ad83e47 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 10 Mar 2021 12:43:59 -0500 Subject: [PATCH 16/65] Create script for testing execution custom data --- tests/resources/functions/package-php-fn.sh | 12 ++++ tests/resources/functions/php-fn.tar.gz | Bin 0 -> 24556 bytes .../resources/functions/php-fn/composer.json | 18 +++++ .../resources/functions/php-fn/composer.lock | 64 ++++++++++++++++++ tests/resources/functions/php-fn/index.php | 31 +++++++++ 5 files changed, 125 insertions(+) create mode 100755 tests/resources/functions/package-php-fn.sh create mode 100644 tests/resources/functions/php-fn.tar.gz create mode 100644 tests/resources/functions/php-fn/composer.json create mode 100644 tests/resources/functions/php-fn/composer.lock create mode 100644 tests/resources/functions/php-fn/index.php diff --git a/tests/resources/functions/package-php-fn.sh b/tests/resources/functions/package-php-fn.sh new file mode 100755 index 000000000..bbb4920cb --- /dev/null +++ b/tests/resources/functions/package-php-fn.sh @@ -0,0 +1,12 @@ + +echo 'PHP Packaging...' + +cp -r $(pwd)/tests/resources/functions/php-fn $(pwd)/tests/resources/functions/packages/php-fn + +docker run --rm -v $(pwd)/tests/resources/functions/packages/php-fn:/app -w /app composer:2.0 composer install --ignore-platform-reqs + +docker run --rm -v $(pwd)/tests/resources/functions/packages/php-fn:/app -w /app appwrite/env-php-8.0:1.0.0 tar -zcvf code.tar.gz . + +mv $(pwd)/tests/resources/functions/packages/php-fn/code.tar.gz $(pwd)/tests/resources/functions/php-fn.tar.gz + +rm -r $(pwd)/tests/resources/functions/packages/php-fn diff --git a/tests/resources/functions/php-fn.tar.gz b/tests/resources/functions/php-fn.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..55dd10889178d4301761a9520337be0c5e2afff5 GIT binary patch literal 24556 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 literal 0 HcmV?d00001 diff --git a/tests/resources/functions/php-fn/composer.json b/tests/resources/functions/php-fn/composer.json new file mode 100644 index 000000000..e3c6db23e --- /dev/null +++ b/tests/resources/functions/php-fn/composer.json @@ -0,0 +1,18 @@ +{ + "name": "appwrite/cloud-function-demo", + "description": "Demo cloud function script", + "type": "library", + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Team Appwrite", + "email": "team@appwrite.io" + } + ], + "require": { + "php": ">=7.4.0", + "ext-curl": "*", + "ext-json": "*", + "appwrite/appwrite": "1.1.*" + } +} diff --git a/tests/resources/functions/php-fn/composer.lock b/tests/resources/functions/php-fn/composer.lock new file mode 100644 index 000000000..758c73c3f --- /dev/null +++ b/tests/resources/functions/php-fn/composer.lock @@ -0,0 +1,64 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "afdff6a172e6c44aee11f1562175f81a", + "packages": [ + { + "name": "appwrite/appwrite", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/appwrite/sdk-for-php.git", + "reference": "98b327d3fd18a72f4582019916afd735a0e9e0e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/98b327d3fd18a72f4582019916afd735a0e9e0e7", + "reference": "98b327d3fd18a72f4582019916afd735a0e9e0e7", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "Appwrite\\": "src/Appwrite" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks", + "support": { + "email": "team@localhost.test", + "issues": "https://github.com/appwrite/sdk-for-php/issues", + "source": "https://github.com/appwrite/sdk-for-php/tree/1.1.2", + "url": "https://appwrite.io/support" + }, + "time": "2020-08-15T18:24:32+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.4.0", + "ext-curl": "*", + "ext-json": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/tests/resources/functions/php-fn/index.php b/tests/resources/functions/php-fn/index.php new file mode 100644 index 000000000..6d4bad1e5 --- /dev/null +++ b/tests/resources/functions/php-fn/index.php @@ -0,0 +1,31 @@ +setEndpoint($_ENV['APPWRITE_ENDPOINT']) // Your API Endpoint + ->setProject($_ENV['APPWRITE_PROJECT']) // Your project ID + ->setKey($_ENV['APPWRITE_SECRET']) // Your secret API key +; + +$storage = new Storage($client); + +// $result = $storage->getFile($_ENV['APPWRITE_FILEID']); + +echo $_ENV['APPWRITE_FUNCTION_ID']."\n"; +echo $_ENV['APPWRITE_FUNCTION_NAME']."\n"; +echo $_ENV['APPWRITE_FUNCTION_TAG']."\n"; +echo $_ENV['APPWRITE_FUNCTION_TRIGGER']."\n"; +echo $_ENV['APPWRITE_FUNCTION_ENV_NAME']."\n"; +echo $_ENV['APPWRITE_FUNCTION_ENV_VERSION']."\n"; +// echo $result['$id']; +echo $_ENV['APPWRITE_FUNCTION_EVENT']."\n"; +echo $_ENV['APPWRITE_FUNCTION_EVENT_PAYLOAD']."\n"; +echo 'data:'.$_ENV['APPWRITE_FUNCTION_DATA']."\n"; +echo 'userId:'.$_ENV['APPWRITE_FUNCTION_USERID']."\n"; +echo 'jwt:'.$_ENV['APPWRITE_FUNCTION_JWT']."\n"; From f56a63c611b94d669059182db398418f7e643038 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 10 Mar 2021 12:48:05 -0500 Subject: [PATCH 17/65] Make JWT for each execution --- app/controllers/api/functions.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index b3243ab93..23273f6ba 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -1,5 +1,7 @@ isEmpty()) { - $jwt = new JWT(App::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. + $newjwt = new JWT(App::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. + $jwt = $newjwt->encode([ + 'userId' => $user->getId(), + 'sessionId' => $session->getId(), + ]); } } From 33fb2e3edd7b1cb801e70b4ffbe96c83cb0a2af4 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 10 Mar 2021 13:05:43 -0500 Subject: [PATCH 18/65] Test for custom data in execution --- .../Functions/FunctionsCustomServerTest.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 9dded8fdd..943e43cfb 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -343,6 +343,37 @@ class FunctionsCustomServerTest extends Scope /** * @depends testCreateExecution */ + public function testCreateCustomExecution($data):array + { + /** + * Test for SUCCESS + */ + $execution = $this->client->call(Client::METHOD_POST, '/functions/'.$data['functionId'].'/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => 'foobar', + ]); + + print_r($execution); + $executionId = $execution['body']['$id'] ?? ''; + + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertNotEmpty($execution['body']['$id']); + $this->assertNotEmpty($execution['body']['functionId']); + $this->assertIsInt($execution['body']['dateCreated']); + $this->assertEquals($data['functionId'], $execution['body']['functionId']); + $this->assertEquals('waiting', $execution['body']['status']); + $this->assertEquals(0, $execution['body']['exitCode']); + $this->assertEquals('', $execution['body']['stdout']); + $this->assertEquals('', $execution['body']['stderr']); + $this->assertEquals(0, $execution['body']['time']); + $this->assertStringContainsString('foobar', $execution['body']['stdout']); + + } + /** + * @depends testCreateCustomExecution + */ public function testListExecutions(array $data):array { /** From 8373db80ef6af38e4cedc962d2137857a45f58f0 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 10 Mar 2021 14:42:36 -0500 Subject: [PATCH 19/65] Comment unneeded function calls --- tests/resources/functions/php-fn.tar.gz | Bin 24556 -> 24556 bytes tests/resources/functions/php-fn/index.php | 14 +++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/resources/functions/php-fn.tar.gz b/tests/resources/functions/php-fn.tar.gz index 55dd10889178d4301761a9520337be0c5e2afff5..02aff814504b80b727fc1777f54abd485eaee4bc 100644 GIT binary patch delta 22843 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 delta 22823 zcmXVXWmp#7_ch(!NQZPuH%O;+mvkvemoRj9OC#MNjlxX{NJw{gH`3hm^8Ehq*Ew_M z?6dY-YoBY+%n1bc1Ol5#0bI4&W5fO~d>algDD<%M^^}&qJ}$AG{^U#6^SPIe=i7d= z!Gz+ngD1RDPcKhT&oz-=xxJoTGlMpz63ge&wD`t9;|IQptTxvV#ucgTyPFS+wd%gz z@#mDy$k+q#hx`Z5EGwNaxw+E?$bEt?;@08EJ5z+dgs6RpX1xQ86M+@LJ8vY9JYV$V z;uCfE`d1KKvHWuS^DEPB{ttigHzp6<1f(OE1D+iMo;B*ePD@16&Jrb$OSJW0g?je| zi_uhLI~vPHv?#vAZd%E7Q!M^#mx!VinZk3DWanI(b*C|LxhLs5wh_ld&ZCmNMilEl z9=~@&I*#9v)}1Rqz64wt-|bBGn9|%l+BQi_`#!lq#@+8s)lOn02Bze@rw?!CLvVZV zH+}vvy;UqP-pvwSNF-iB}lRv(T0UY^mYrQsO@jy@5qX27(tY=#7oq(g|R%eLro* z`SV|Ou7?r+n6S+8|I*1i0Yj6I`}>7Rr8_*2ljPiYJMmBAWg=$p7Xr~St4G?{hl%Sa z(&a|(MdOYUc>9t;*mHDCJBd_|Ko9c6+i6A7Hx7W7G69{G^ji_K6YItY9 z4~^SbC~qPg7Jg?K8gE6Gv~|>F{QWs8;B@4R6A7Q=DnW{2c!2;W$d{LBD4o1fb!0+ul71RzU_yE=JN!jQH_W%1O9-D78!Q1#Ut9!=wb-}x#iF=5 z+akhf5qdTwEVgC-^1vGt4teCjI1^Hv8%8(|lD9=)eo>yeV$(RuZF-10)n)QrO#d!X zogsffgggVmJP2KBQDR|Xu=(>AhrWd;K}a+ScJat*+%VyDN5{jmp&_Hsnx!Ace{U(- ztQ36PySx6*a~vYeDpfLQvKQo}!W!4p5lCXEG*B28AN&d@-AAW+47v0sDP{43o4^)DfsDxMqj)m>dR(-s7+xR9xEaJh1lr&Jn^F_2vd4 z)1%AQYwr(HlcWT!>iSsUL|RV~F;CQ(hjrn6vmVaeU_U5Ws2i2x)gQmoGzkC7pRa05 zRpvIByRu#Our)0O1sWI27Ef62c)3LkQ8>TvuOx}8&oTM(rq+_jN7&bAnH&y%k-~#9 z%UJ+M;i$z2-?8ll?2`-O`H8zzkqD02P9GY~78E5udG%gUyx-_d~*i->)0Y zB#GJ9wplpL6!qD|qkevjkPIz#Ob|t_S$}cJ>ueWf63BKoYK});uN;#*t#-SBCR%f! zPz;dm+QiAimr`f3pcw>CeBuk>X61qT9$2EAiv=2}5QVplDUIZilA??d)0khpR{?In zFb5gdniiWr({)N2v&FvrCGJ^xFD0`&Eg?C2jR%(&rf4>IWK)Bd(yMNcDcUlz`CL6! z!S^@dx9I?s^9K-F^S&_@azULvvg_9F_ZNK1Bd$Get$5+| zRsH9`1Gf$(pI-(=PXHPtwv$^>y!Q7C#9wZ~0YooZQRFv65D$Fe%!4620d7IcTD~VU zA02TKzT-G8OS^SDtSd0)_v}+X>)%sQvB7=)0fZbvC9Kk5y?Xp={9fwfM$S4w$GGQt zN5bps&ud>_tuCL_{8@qy9DJ*eD)In{sM?BS=sLlcx*Za>^7h0h78tv1517QK}gugM30COc70 zHonC|vzt&4Rl8%_8z=ol#4h}I=3YM!(?5vsBOwj4C@#~vZ^i-vfQyj;&z2O-Ps)W% zO%u5DwN@+wL&a9e7%R+~iT)}^q1y^oNk*X?@AMtD=dw3fC;xKeK<3Q;B$9RDEoIaF zry=Q0v@FIp-V4(u+HtT8mt2bp%hO^Z_h|l%+)Bdo;?fuj}z0ry2beO5KsTv!$)$#V7v!6B1 z@p^Ny@gy-|dSAT~zIl}1kvhKi?s|PJebK2=E@swL;Fu2v{TgT*w$062{LrvuP!eET zlbc&g5@=x?hgCIiD&1ygohuhzYDPZrhI7%eF$`)@_WWJ9&SP!zE2m>FPtNgM6skr#Xv;&L+W|scwPApXcv!R8 zk|f?j?y%3u*Em6Vilu$drCh`PUzR_n<%*O-ow92LKK@)9*YBKUaL8>PBj`iGRK=hU z)Ske&$=*)!V$jkS55Pn^g@*(naIpclk*7I zz7RX<9ZJ1sE9#o+@lvG2-iBz|42mX^^nuPac7W!F!z`~2@s%X7LbDsctxi!i_4(Tip zI_w~6DUfMk9W#u|A?K@doh@Go^CneD&LgVwVAR?&Dk$iNw0Fme>xhX09UD5b7%%0= zg*_kR;y1do*&t-zdj07{k+_!!B*x=3zTSY;gC>_*G;D&~M}9}@YV3YxVqFW0Y6&E- zi#Kgn9r5#H7*Xdu4a%^(Sh1rt3i@h_Fy6J@(K&H}5mO3=xS>cI8pkD_ouyK2I7)aN z$!R>J-{wulAybGJ-n2!>FE*F4&b*$&_hQ=3p@EsSdG3>}jBD?@T8_HX9OGUf&BOpa z{{m=So0{JUmROv5+rE}eLmf~;uk3M*FvovW8F6HIx`laHN%w6Ht3@`mrlDa=+jnD^ zD(>IoJR1U?Wr;t1zRgzpj;ng)<5Do_@a78wFNZysbrid<@>Wp-S*zqa-{T)kk;}Wb#ExKzFm$FA8`I zQYVA81x%ixf}9Hinh>c|R{Mw$0vW~B&+_0-!aI3OqH*O+gClZ6+i)1i&_!EZ3w6v) zN(veSsvit@J(I!rvB75TF3zT_iF67SF5g~!sLvDurt z)iAU!aRmH>7Y&D?rqbKx-(j_T^TDZgR{dj#93-R2CmBj_X0 ze}+v&WER9f3hhhp8QM%iu&koT@JB8~UH9y>tO~N_@$jYf=ikEL0=)i--fKh!OOe`U zN6)17;|t-&`&;-}s1Y!zT!gxX+&sjqQJ8d}LxR8*vI?t$!=Owrno7tH*2#_*63W+p zp5I+xlCYQyg4?O}d3-k!>jvxS5gOqbAD4ht`EZJ9%iS>|)Kp6Q>_R z|7zhh!)zp$y>E<@0|Gxr2MwOI+dER5Zda z62moQ@j_b_Bi#Q!D#`*H+q~(eQNppUPemP7V@%QePWEyATKA>afO4xXjU4ohM*8_i zA;ul`=a%xETvhM2)|h|8t+d~P+JbV#9VB-2o_kUi15)yi8jv)zf?kipzDVDi1^rNX z1kri3OcSPTnmt3(awh}Ju6bCkSv1Qelz7fGXU#(& zC9v3P!SyA zD=S=MZ=JBvZBTZz=`?BLl)-(g>Q*(MBVDEB}iRig>_JY|2=uS<*)<%w1utdEojf4GEW;fALY#%SlGX07&x z6>%f;DFDhGyS{%R|16d(B0oJ%pA=_-{bNF=oW)tN4plCu zYrkKzPlJp$GlLP%ogO_*EA+5!;$X)3{txthpycH2`P{TNqPo({n*mmTkb5e6Uobde zg0(H(I?9{D@-2)EKhhXs!iaXm8C+VQ{@bIiGr;jP`e8$!yHr}`Me~o~_m~+jeoPHh zJ@G6S8KqJ-?T3c0_;-m3$= zTjIi^@FVjixuVflk}rFqT1S}DxW#4HYqx@m>suI=&r9m^qJ@3sc1HE*KSO?oITD`K z1Ilb1hKqFRXY2YHi-|olK6)C=55znKV3?hx!H460ueU1BPUlG14@sLQedJl zm8#z{JU$Gql~??A4mRH%B@}r!bQSzNeWu}(Pk1n6XWU-7xh=$)RxBDlhS0l%%H^Iu*XrDw? zMD^XT_EB6rMGaRpRwrp`s$^KYdV`xG&1l zGKNnUi}smsvG4EvCB7noB9q|bq+5xK55b;n==O(#HQUW@dKy0~ayaHhMrjYhZfTuI zs!oW|u9iC2oFp8)%qZV!%*ew~LyLfSrB;aGT-QDVxuLVl3p?3xJcT~$rHH43PgWD35_m7qNESGaOOW*+RbY*B=^RwZ@k1} zyl+!v*IG^AFk=}bMe*`gQiF23x~!N`jJTdUsiV%>5p9uUsaC2NY$c@$#(o!P*@9?^ z?x|iY8FwGMHE#glS978TC_MkR5zEULm|Cxk2`g>Bpn$w^cBtxg^73?M<#iQrb>rn$ z_ks!gWp16DySi$ISg&o}Me^}tT_81~1-Uc&BGMPX!>QNm(vB-RG_bEx|MJyJwrlx# z=#4fnUp}o~=5Alyxsc;6PR&mFZyO&nEEm38So&JNV#ftw2JB&220Ei5Tx;ZT&N!>9 zQ@($h(WT|n`=(fiK+Q_P-C>G?;u)4Vy+&GCXB}#jz<;^V<1F#>gnGhx{(#Bw>nU34 zsz-{W)=U0G`*QX#lz^8zh_dYcLMP~tJwNwEl@XU!Y*8@%$QaaWK`9 z>bIP=p$b4i6E@26Q($w)>h5X%&h-!iy2U}xiU7_HLtvHhaat64(DOXg$a9WEyyXfx z8>AlZNm;>Xb>64E>lpN$ABX@&tK6iVY?^-k0uIJwvyg@aCrv;KzQH-y`sOfO{YueloBZ#BXzHE!DF=O zux>g|g zxHoX?s|d;#CqwZM^W`zS3t+q9N4zA%a3(To2g2;w+#5ztSToZQ4z~0tnFd z%QVy02n1ts*&Q@)Azn{H3b9^BiD31UI!5!9O?HBv{E1u-Zv&ChojGJPvI=ckW< zbrBj5fGWaf(8;NF_@5a~MKNf9!|4SB_wPKHoTB;e!gJ{FCKR|fhkrQCKNmHreqN9L z1O7{fqAR|AVVss2yh?md_!4Mc>MJp(3dW>@j$|$mW{<#s!)isK{T0Nu8TjE9b*IkB zBQm50Ho=Z=$5eUC(S#sdjerNx@tQGBkdxsRWfiauc~sg}bmdxwFz{&#l)44Zs%3X2 zazUm;;D6xqOn^Kk{wmj#0M;%TVQ>SMgSKupzrglP?J0oeMFM!e>T^Ifeu+#FNLm{{ z20$&VnstxBu-8nd98xuiegS1Ye3-xbW~D%16bP}4K<8#95cqG;?vHo9av6_6Ae527 z-!TaascAmV1f3h3!2jh0CreQUBSD)vR~H;b7r&d|7OWn^G2HSrvZ0x zUe3|2tQe$yUH0AMy7Uc{(vE6c^ro)H)6E1qPZiX7arS==o#2&j36mi7=XuyYkeL7l zGwV@}PTwuhJ_)bQr#D~?6@z07596o0_8>ET+6`3^2+;YSBi26&HHJJKLL}bVuD~R# zf|u83z%F#$;&rt@Hvo0RlMp_LeOCyQVg*F7Q@x-*Hxl}9vK!^V%n=YCq6{^Y86F4iO`7P-0HG-~6rl~~=lp#F z7m(f;Ku-QB{+ORxTf<|UR`Y?#0*o32R~Fe0A(5|9z`dZ~IGKq9NM9r#sj%63j&BCp zZUCwnuv23IUcKtN(|Jt31O@e{22=xsjwVMTEHuv^ zi038lJS_LbBn_ScR!;y5SiY{Jr}Da|-?weMI57Wbxdgb>toDFb*X4fB9YIfKMMu-?3Ds8FUPcpfE#NNR2g*sPkDkbZUp-T zfX5f_nE+Eg@lZ{tM|pT0Jh0McH7mTt0bLMmrML#b*RJ;e#e7JmP96oAQQtS(z`gQl z!A3y%r%69F;Z*k-diQE2{}E9MEFYxqgxRim#2JEYPe zCzWgm>4KoY|0SR|gU*AeAldFCpp)$C-diZ zS8%YeEa#fj6Np_XANgcko?x2<`|U}>;?j&?pjG*mf2sMP88s~%@*QIiAq?IEb58!% zAAc$;(G{dKU~&z3p+0}IrqdA!z(trmFrcHsW09Vx5UI-^+bFY6!gHGh{!(;*dt8^B zfuf^#Pk~0Snt3fMlK%#OQZ0=y?BS~j9h#j$4PV`RMNtqmoBGf6fPAQsYuPYZ)wi7x zK|DT6DN6jpX$T!WfgF>OSwHXp=V}^{*qSemG{E&tY6^7n=@=c9%>`GLB0-(J|3Q5| zEkWJN`J8(mg1u7)t_%YE`XKy6#wFxGz# zWdg4WMtc!?k8}}>BA)yj@*{xh$n~`NTD+emzCS_cS_MX4sQx37|0D8_INSoUqM*iqiID-zBA3_k{}baiGbl0Y22La8g8%;nLGuW9rGiv@ zh5r@6)287Uv)9K0Q0f1?r}H(9kop%hkylH5VDii+@+Gqppg}e(e_l@_+0fVHN>MCmrv5*E?1wJn-hoSQCYV-}U}8nM z$S7nFyA;Fb6KnX-mj)q)UtGIOpONl8r1_BZA4jk>sG~v4?1%uvoh` z@7Hag2B3Dg#6||*Kg>8D~ zpiUEb6daXqv*^i>dSos(o)4S>9y+}i_nc(uVs>*M)4w*5 zzG?@I`hN%x4u1v@--X<43jA03UKlrZXLw2&xksekL(U%38cg>O!-#<+a@MQ?H0BUm z%TWgmL_5;}@4|#VxT)W+S|9OONEG8KxV;m_Z)`W*S#Wv40pxIN#evSQ3A&D%2( z!%WY{2+k0aL>GSk_6XLOskEk1s?cwImxV`y zp~8!Q*IGz^na~BsaT3{yTRI$FWdo(fg0uwE9-1k zpVq;TU9Sr)_lwy+t^7s%RUQA>h-e2sW~6v`b)h@@nTI(M@+q5fPImb`2SoAmH{lZW zf4@Jz38L7Swsi~UKxdTZ=n_ZSI2}RkS+hzx+<1zPFF;5Hh-edu4ohG>pRUXq<%K8= z8A5DNyK*TkWl66~)EQ`dkfnq_bYB0>F%xbeTeIDuvKh~`E6-L6hh^y>;ywyqkBy6} z>BZOM@qI*bmr|uFNSIOJD$yqoMm4C zN0H^4DX1Qx@Tka2NWSk2MHQcq!Bi1Iq z>Xai%Q}s;ku(E$cPP}F<*H4xH+O83Y`X%1WtGh)|rpKJD!t7?!s<_(ufe^qs|A;eh z$o@bH49<2SI`P`H$lxQuDbbSXLa?h!Wd!dTyOdJ7aJ1XH9r@Hzckz|m0&^)V7-*06 zDGnGa^cU1)S}o!G2;k4+7E!wh0>AUO_;n$ZroHgL%zl}CN;3_3+rM5!w-Ng2GAbb^ zG#>T%T{ult-Q60`Bkc?5X~+EB-!4rOmH&VMl;h98dA~1-6_`E|b`@WQtwQpV1cs?F ziGFkBYg#PYa6j(_wl7$mVAI2m+M#vrIaz7-jYT~;i=b|PD8?;{iV$0Z7pLj z4qO;a`EKaM$vH``R7(W;TrCX8RgyUnKN3}%-Na)5!!X$<-9I#v`XeR0Pul!1C#D~# znHTJrh*)Y(?58${4VL9|usBjLacxW3!MD2Z_3B3$hx<_c47pC(o_L%rVwZFz^eCrI zd{IZ|F4U|0`%%R`+kQfby!vCpucTk}cYxcQ^Y$DsF4<=0Z66EQ#^_qm2LO z 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 VSzl2BMw6>wIS4jJt}g&c0|1MZtAqdm diff --git a/tests/resources/functions/php-fn/index.php b/tests/resources/functions/php-fn/index.php index 6d4bad1e5..8490d2b1c 100644 --- a/tests/resources/functions/php-fn/index.php +++ b/tests/resources/functions/php-fn/index.php @@ -5,15 +5,15 @@ include './vendor/autoload.php'; use Appwrite\Client; use Appwrite\Services\Storage; -$client = new Client(); +// $client = new Client(); -$client - ->setEndpoint($_ENV['APPWRITE_ENDPOINT']) // Your API Endpoint - ->setProject($_ENV['APPWRITE_PROJECT']) // Your project ID - ->setKey($_ENV['APPWRITE_SECRET']) // Your secret API key -; +// $client + // ->setEndpoint($_ENV['APPWRITE_ENDPOINT']) // Your API Endpoint + // ->setProject($_ENV['APPWRITE_PROJECT']) // Your project ID + // ->setKey($_ENV['APPWRITE_SECRET']) // Your secret API key +// ; -$storage = new Storage($client); +// $storage = new Storage($client); // $result = $storage->getFile($_ENV['APPWRITE_FILEID']); From 36b8e6defbe934fe45f156848b8ef62e57a43e7b Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 10 Mar 2021 14:43:15 -0500 Subject: [PATCH 20/65] Server test assertions --- .../Functions/FunctionsCustomServerTest.php | 104 ++++++++++++------ 1 file changed, 73 insertions(+), 31 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 943e43cfb..fbd7ec141 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -343,37 +343,6 @@ class FunctionsCustomServerTest extends Scope /** * @depends testCreateExecution */ - public function testCreateCustomExecution($data):array - { - /** - * Test for SUCCESS - */ - $execution = $this->client->call(Client::METHOD_POST, '/functions/'.$data['functionId'].'/executions', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => 'foobar', - ]); - - print_r($execution); - $executionId = $execution['body']['$id'] ?? ''; - - $this->assertEquals(201, $execution['headers']['status-code']); - $this->assertNotEmpty($execution['body']['$id']); - $this->assertNotEmpty($execution['body']['functionId']); - $this->assertIsInt($execution['body']['dateCreated']); - $this->assertEquals($data['functionId'], $execution['body']['functionId']); - $this->assertEquals('waiting', $execution['body']['status']); - $this->assertEquals(0, $execution['body']['exitCode']); - $this->assertEquals('', $execution['body']['stdout']); - $this->assertEquals('', $execution['body']['stderr']); - $this->assertEquals(0, $execution['body']['time']); - $this->assertStringContainsString('foobar', $execution['body']['stdout']); - - } - /** - * @depends testCreateCustomExecution - */ public function testListExecutions(array $data):array { /** @@ -791,4 +760,77 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals($executions['body']['executions'][0]['stdout'], ''); $this->assertEquals($executions['body']['executions'][0]['stderr'], ''); } + + /** + * @depends testTimeout + */ + public function testCreateCustomExecution() + { + $name = 'php-8.0'; + $code = realpath(__DIR__ . '/../../../resources/functions').'/php-fn.tar.gz'; + $command = 'php index.php'; + $timeout = 2; + + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Test '.$name, + 'env' => $name, + 'vars' => [], + 'events' => [], + 'schedule' => '', + 'timeout' => $timeout, + ]); + + $functionId = $function['body']['$id'] ?? ''; + + $this->assertEquals(201, $function['headers']['status-code']); + + $tag = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/tags', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'command' => $command, + 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + ]); + + $tagId = $tag['body']['$id'] ?? ''; + $this->assertEquals(201, $tag['headers']['status-code']); + + $tag = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/tag', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'tag' => $tagId, + ]); + + $this->assertEquals(200, $tag['headers']['status-code']); + + $execution = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => 'foobar', + ]); + + $executionId = $execution['body']['$id'] ?? ''; + + $this->assertEquals(201, $execution['headers']['status-code']); + + sleep(10); + + $executions = $this->client->call(Client::METHOD_GET, '/functions/'.$functionId.'/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($executions['headers']['status-code'], 200); + $this->assertEquals($executions['body']['sum'], 1); + $this->assertIsArray($executions['body']['executions']); + $this->assertCount(1, $executions['body']['executions']); + $this->assertEquals($executions['body']['executions'][0]['$id'], $executionId); + $this->assertEquals($executions['body']['executions'][0]['trigger'], 'http'); + $this->assertStringContainsString('foobar', $executions['body']['executions'][0]['stdout']); + } } From 681ab12f7660ba38f7fbb1ca399b3432ffbd0d84 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 10 Mar 2021 15:25:54 -0500 Subject: [PATCH 21/65] Instantiate jwt outside conditionals --- app/controllers/api/functions.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 23273f6ba..6c9bd6192 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -735,12 +735,12 @@ App::post('/v1/functions/:functionId/executions') if (false === $execution) { throw new Exception('Failed saving execution to DB', 500); } - + + $jwt = ''; // initialize if (!empty($user->getId())) { // If userId exists, generate a JWT for function $tokens = $user->getAttribute('tokens', []); $session = new Document(); - $jwt = ''; foreach ($tokens as $token) { /** @var Appwrite\Database\Document $token */ if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too @@ -749,8 +749,8 @@ App::post('/v1/functions/:functionId/executions') } if(!$session->isEmpty()) { - $newjwt = new JWT(App::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. - $jwt = $newjwt->encode([ + $jwtObj = new JWT(App::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. + $jwt = $jwtObj->encode([ 'userId' => $user->getId(), 'sessionId' => $session->getId(), ]); From 034679930aa7bec023d212495472c8b4f8897abb Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 10 Mar 2021 15:50:20 -0500 Subject: [PATCH 22/65] Add client e2e test --- .../Functions/FunctionsCustomClientTest.php | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index 25d3a8237..74f0a7d80 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -115,4 +115,78 @@ class FunctionsCustomClientTest extends Scope return []; } -} \ No newline at end of file + + public function testCreateCustomExecution():array + { + /** + * Test for SUCCESS + */ + $function = $this->client->call(Client::METHOD_POST, '/functions', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'name' => 'Test', + 'execute' => ['*'], + 'env' => 'php-7.4', + 'vars' => [ + 'funcKey1' => 'funcValue1', + 'funcKey2' => 'funcValue2', + 'funcKey3' => 'funcValue3', + ], + 'events' => [ + 'account.create', + 'account.delete', + ], + 'schedule' => '* * * * *', + 'timeout' => 10, + ]); + + $this->assertEquals(201, $function['headers']['status-code']); + + $tag = $this->client->call(Client::METHOD_POST, '/functions/'.$function['body']['$id'].'/tags', [ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'command' => 'php function.php', + 'code' => new CURLFile(realpath(__DIR__ . '/../../../resources/functions/php-fn.tar.gz'), 'application/x-gzip', 'php-fx.tar.gz'), //different tarball names intentional + ]); + + $tagId = $tag['body']['$id'] ?? ''; + + $this->assertEquals(201, $tag['headers']['status-code']); + + $function = $this->client->call(Client::METHOD_PATCH, '/functions/'.$function['body']['$id'].'/tag', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'tag' => $tagId, + ]); + + $this->assertEquals(200, $function['headers']['status-code']); + + $execution = $this->client->call(Client::METHOD_POST, '/functions/'.$function['body']['$id'].'/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => 'foobar', + ]); + + $executionId = $execution['body']['$id'] ?? ''; + + $this->assertEquals(201, $execution['headers']['status-code']); + + $execution = $this->client->call(Client::METHOD_GET, '/functions/'.$function['body']['$id'].'/executions/'.$executionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $execution['headers']['status-code']); + $this->assertStringContainsString('foobar', $execution['body']['stdout']); + $this->assertStringContainsString($this->getUser()['$id'], $execution['body']['stdout']); + + return []; + } +} From a737b23fa536861db3d5860c486d08b0738dfcc2 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Thu, 11 Mar 2021 07:39:06 -0500 Subject: [PATCH 23/65] Add modal for execution data --- app/views/console/functions/function.phtml | 46 +++++++++++++--------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/app/views/console/functions/function.phtml b/app/views/console/functions/function.phtml index 4ae65f491..5ad3db319 100644 --- a/app/views/console/functions/function.phtml +++ b/app/views/console/functions/function.phtml @@ -50,24 +50,9 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);

- -
-   View Logs -
+
+   View Logs +
@@ -575,6 +560,31 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true); +