From 311e9a75dd4d31942cf7f2318b6c413abcdfac79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 20 Jan 2024 09:43:31 +0000 Subject: [PATCH] Allow empty values in PATH user service --- app/controllers/api/users.php | 18 ++- src/Appwrite/Auth/Validator/Password.php | 11 ++ .../Auth/Validator/PasswordDictionary.php | 3 +- src/Appwrite/Auth/Validator/Phone.php | 17 ++- src/Appwrite/Network/Validator/Email.php | 11 ++ tests/e2e/Services/Users/UsersBase.php | 103 ++++++++++++++++++ 6 files changed, 157 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index e090b32300..1764791073 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -1033,7 +1033,7 @@ App::patch('/v1/users/:userId/name') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_USER) ->param('userId', '', new UID(), 'User ID.') - ->param('name', '', new Text(128), 'User name. Max length: 128 chars.') + ->param('name', '', new Text(128, 0), 'User name. Max length: 128 chars.') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -1071,7 +1071,7 @@ App::patch('/v1/users/:userId/password') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_USER) ->param('userId', '', new UID(), 'User ID.') - ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary']) + ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, enabled: $project->getAttribute('auths', [])['passwordDictionary'] ?? false, allowEmpty: true), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary']) ->inject('response') ->inject('project') ->inject('dbForProject') @@ -1091,6 +1091,16 @@ App::patch('/v1/users/:userId/password') } } + if (\strlen($password) === 0) { + $user + ->setAttribute('password', '') + ->setAttribute('passwordUpdate', DateTime::now()); + + $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $queueForEvents->setParam('userId', $user->getId()); + $response->dynamic($user, Response::MODEL_USER); + } + $newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); $historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0; @@ -1136,7 +1146,7 @@ App::patch('/v1/users/:userId/email') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_USER) ->param('userId', '', new UID(), 'User ID.') - ->param('email', '', new Email(), 'User email.') + ->param('email', '', new Email(allowEmpty: true), 'User email.') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -1211,7 +1221,7 @@ App::patch('/v1/users/:userId/phone') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_USER) ->param('userId', '', new UID(), 'User ID.') - ->param('number', '', new Phone(), 'User phone number.') + ->param('number', '', new Phone(allowEmpty: true), 'User phone number.') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') diff --git a/src/Appwrite/Auth/Validator/Password.php b/src/Appwrite/Auth/Validator/Password.php index ffb72467e5..bfe5577889 100644 --- a/src/Appwrite/Auth/Validator/Password.php +++ b/src/Appwrite/Auth/Validator/Password.php @@ -11,6 +11,13 @@ use Utopia\Validator; */ class Password extends Validator { + protected bool $allowEmpty; + + public function __construct(bool $allowEmpty = false) + { + $this->allowEmpty = $allowEmpty; + } + /** * Get Description. * @@ -36,6 +43,10 @@ class Password extends Validator return false; } + if ($this->allowEmpty && \strlen($value) === 0) { + return true; + } + if (\strlen($value) < 8) { return false; } diff --git a/src/Appwrite/Auth/Validator/PasswordDictionary.php b/src/Appwrite/Auth/Validator/PasswordDictionary.php index e128f497f5..99a6c81525 100644 --- a/src/Appwrite/Auth/Validator/PasswordDictionary.php +++ b/src/Appwrite/Auth/Validator/PasswordDictionary.php @@ -12,8 +12,9 @@ class PasswordDictionary extends Password protected array $dictionary; protected bool $enabled; - public function __construct(array $dictionary, bool $enabled = false) + public function __construct(array $dictionary, bool $enabled = false, bool $allowEmpty = false) { + parent::__construct($allowEmpty); $this->dictionary = $dictionary; $this->enabled = $enabled; } diff --git a/src/Appwrite/Auth/Validator/Phone.php b/src/Appwrite/Auth/Validator/Phone.php index 32c3ca3398..b8c66edd07 100644 --- a/src/Appwrite/Auth/Validator/Phone.php +++ b/src/Appwrite/Auth/Validator/Phone.php @@ -11,6 +11,13 @@ use Utopia\Validator; */ class Phone extends Validator { + protected bool $allowEmpty; + + public function __construct(bool $allowEmpty = false) + { + $this->allowEmpty = $allowEmpty; + } + /** * Get Description. * @@ -32,7 +39,15 @@ class Phone extends Validator */ public function isValid($value): bool { - return is_string($value) && !!\preg_match('/^\+[1-9]\d{1,14}$/', $value); + if (!is_string($value)) { + return false; + } + + if ($this->allowEmpty && \strlen($value) === 0) { + return true; + } + + return !!\preg_match('/^\+[1-9]\d{1,14}$/', $value); } /** diff --git a/src/Appwrite/Network/Validator/Email.php b/src/Appwrite/Network/Validator/Email.php index efc1a5d5b3..3209a4aada 100644 --- a/src/Appwrite/Network/Validator/Email.php +++ b/src/Appwrite/Network/Validator/Email.php @@ -13,6 +13,13 @@ use Utopia\Validator; */ class Email extends Validator { + protected bool $allowEmpty; + + public function __construct(bool $allowEmpty = false) + { + $this->allowEmpty = $allowEmpty; + } + /** * Get Description * @@ -35,6 +42,10 @@ class Email extends Validator */ public function isValid($value): bool { + if ($this->allowEmpty && \strlen($value) === 0) { + return true; + } + if (!\filter_var($value, FILTER_VALIDATE_EMAIL)) { return false; } diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php index ddd6e22470..13f010c169 100644 --- a/tests/e2e/Services/Users/UsersBase.php +++ b/tests/e2e/Services/Users/UsersBase.php @@ -742,6 +742,32 @@ trait UsersBase /** * Test for SUCCESS */ + $user = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($user['headers']['status-code'], 200); + $this->assertEquals($user['body']['name'], 'Cristiano Ronaldo'); + + $user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/name', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => '', + ]); + + $this->assertEquals($user['headers']['status-code'], 200); + $this->assertEquals($user['body']['name'], ''); + + $user = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($user['headers']['status-code'], 200); + $this->assertEquals($user['body']['name'], ''); + $user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/name', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -809,6 +835,32 @@ trait UsersBase /** * Test for SUCCESS */ + $user = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($user['headers']['status-code'], 200); + $this->assertEquals($user['body']['email'], 'cristiano.ronaldo@manchester-united.co.uk'); + + $user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/email', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'email' => '', + ]); + + $this->assertEquals($user['headers']['status-code'], 200); + $this->assertEquals($user['body']['email'], ''); + + $user = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($user['headers']['status-code'], 200); + $this->assertEquals($user['body']['email'], ''); + $user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/email', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -876,6 +928,37 @@ trait UsersBase /** * Test for SUCCESS */ + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'email' => 'users.service@updated.com', + 'password' => 'password' + ]); + + $this->assertEquals($session['headers']['status-code'], 201); + + $user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/password', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'password' => '', + ]); + + $this->assertEquals($user['headers']['status-code'], 200); + $this->assertNotEmpty($user['body']['$id']); + $this->assertEmpty($user['body']['password']); + + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'email' => 'users.service@updated.com', + 'password' => 'password' + ]); + + $this->assertEquals($session['headers']['status-code'], 401); + $user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/password', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -885,6 +968,7 @@ trait UsersBase $this->assertEquals($user['headers']['status-code'], 200); $this->assertNotEmpty($user['body']['$id']); + $this->assertNotEmpty($user['body']['password']); $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ 'content-type' => 'application/json', @@ -1022,6 +1106,25 @@ trait UsersBase /** * Test for SUCCESS */ + $updatedNumber = ""; + $user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/phone', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'number' => $updatedNumber, + ]); + + $this->assertEquals($user['headers']['status-code'], 200); + $this->assertEquals($user['body']['phone'], $updatedNumber); + + $user = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($user['headers']['status-code'], 200); + $this->assertEquals($user['body']['phone'], $updatedNumber); + $updatedNumber = "+910000000000"; //dummy number $user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/phone', array_merge([ 'content-type' => 'application/json',