diff --git a/app/config/collections.php b/app/config/collections.php index 2f630e0ba1..5720b66d4f 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1433,6 +1433,13 @@ $collections = [ ], ], ], + Database::SYSTEM_COLLECTION_RESERVED => [ + '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, + '$id' => Database::SYSTEM_COLLECTION_RESERVED, + '$permissions' => ['read' => ['*']], + 'name' => 'Reserved', + 'structure' => true, + ], ]; /* diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index f195320d6b..551fffb643 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -526,3 +526,48 @@ App::delete('/v1/users/:userId/sessions') $response->json(array('result' => 'success')); }, ['response', 'projectDB']); + +App::delete('/v1/users/:userId') + ->desc('Delete User') + ->groups(['api', 'users']) + ->label('scope', 'users.write') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'deleteUser') + ->label('sdk.description', '/docs/references/users/delete-user.md') + ->label('abuse-limit', 100) + ->param('userId', '', function () {return new UID();}, 'User unique ID.') + ->action(function ($userId, $response, $projectDB, $deletes) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $deletes */ + + $user = $projectDB->getDocument($userId); + + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); + } + if (!$projectDB->deleteDocument($userId)) { + throw new Exception('Failed to remove user from DB', 500); + } + + if (!$projectDB->deleteUniqueKey(md5('users:email='.$user->getAttribute('email', null)))) { + throw new Exception('Failed to remove unique key from DB', 500); + } + + $reservedId = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_RESERVED, + '$id' => $userId, + '$permissions' => [ + 'read' => ['*'], + ], + ]); + + if (false === $reservedId) { + throw new Exception('Failed saving reserved id to DB', 500); + } + + $deletes->setParam('document', $user); + + $response->noContent(); + }, ['response', 'projectDB', 'deletes']); diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 015b8cedd1..c8f423c641 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -30,6 +30,9 @@ class DeletesV1 case Database::SYSTEM_COLLECTION_PROJECTS: $this->deleteProject($document); break; + case Database::SYSTEM_COLLECTION_USERS: + $this->deleteUser($document); + break; default: break; @@ -58,4 +61,32 @@ class DeletesV1 $uploads->delete($uploads->getRoot(), true); $cache->delete($cache->getRoot(), true); } + + protected function deleteUser(Document $user) + { + global $projectDB; + + $tokens = $user->getAttribute('tokens', []); + + foreach ($tokens as $token) { + if (!$projectDB->deleteDocument($token->getId())) { + throw new Exception('Failed to remove token from DB', 500); + } + } + + $memberships = $projectDB->getCollection([ + 'limit' => 2000, // TODO add members limit + 'offset' => 0, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, + 'userId='.$user->getId(), + ], + ]); + + foreach ($memberships as $membership) { + if (!$projectDB->deleteDocument($membership->getId())) { + throw new Exception('Failed to remove team membership from DB', 500); + } + } + } } diff --git a/docs/references/users/delete-user.md b/docs/references/users/delete-user.md new file mode 100644 index 0000000000..7eb4963485 --- /dev/null +++ b/docs/references/users/delete-user.md @@ -0,0 +1 @@ +Delete a user by its unique ID. \ No newline at end of file diff --git a/src/Appwrite/Database/Adapter.php b/src/Appwrite/Database/Adapter.php index 1bd817c124..4dc7152571 100644 --- a/src/Appwrite/Database/Adapter.php +++ b/src/Appwrite/Database/Adapter.php @@ -88,6 +88,15 @@ abstract class Adapter */ abstract public function deleteDocument($id); + /** + * Delete Unique Key. + * + * @param int $key + * + * @return array + */ + abstract public function deleteUniqueKey($key); + /** * Create Namespace. * diff --git a/src/Appwrite/Database/Adapter/MySQL.php b/src/Appwrite/Database/Adapter/MySQL.php index 378a4d6b81..dffa39b7bf 100644 --- a/src/Appwrite/Database/Adapter/MySQL.php +++ b/src/Appwrite/Database/Adapter/MySQL.php @@ -350,6 +350,26 @@ class MySQL extends Adapter return []; } + /** + * Delete Unique Key. + * + * @param int $key + * + * @return array + * + * @throws Exception + */ + public function deleteUniqueKey($key) + { + $st1 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.unique` WHERE `key` = :key'); + + $st1->bindValue(':key', $key, PDO::PARAM_STR); + + $st1->execute(); + + return []; + } + /** * Create Relation. * diff --git a/src/Appwrite/Database/Adapter/Redis.php b/src/Appwrite/Database/Adapter/Redis.php index a3b12e2ecc..7fe6ee0689 100644 --- a/src/Appwrite/Database/Adapter/Redis.php +++ b/src/Appwrite/Database/Adapter/Redis.php @@ -153,6 +153,22 @@ class Redis extends Adapter return $data; } + /** + * Delete Unique Key. + * + * @param $key + * + * @return array + * + * @throws Exception + */ + public function deleteUniqueKey($key) + { + $data = $this->adapter->deleteUniqueKey($key); + + return $data; + } + /** * Create Namespace. * diff --git a/src/Appwrite/Database/Database.php b/src/Appwrite/Database/Database.php index 45e936207c..4c62307b07 100644 --- a/src/Appwrite/Database/Database.php +++ b/src/Appwrite/Database/Database.php @@ -23,6 +23,7 @@ class Database const SYSTEM_COLLECTION_USAGES = 'usages'; //TODO add structure const SYSTEM_COLLECTION_DOMAINS = 'domains'; const SYSTEM_COLLECTION_CERTIFICATES = 'certificates'; + const SYSTEM_COLLECTION_RESERVED = 'reserved'; // Auth, Account and Users (private to user) const SYSTEM_COLLECTION_USERS = 'users'; @@ -344,6 +345,18 @@ class Database return new Document($this->adapter->deleteDocument($id)); } + /** + * @param int $key + * + * @return Document|false + * + * @throws AuthorizationException + */ + public function deleteUniqueKey($key) + { + return new Document($this->adapter->deleteUniqueKey($key)); + } + /** * @return array */ diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php index f66841ce05..c4c99de29e 100644 --- a/tests/e2e/Services/Users/UsersBase.php +++ b/tests/e2e/Services/Users/UsersBase.php @@ -168,6 +168,34 @@ trait UsersBase return $data; } + /** + * @depends testGetUser + */ + public function testDeleteUser(array $data):array + { + /** + * Test for SUCCESS + */ + $user = $this->client->call(Client::METHOD_DELETE, '/users/' . $data['userId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($user['headers']['status-code'], 204); + + /** + * Test for FAILURE + */ + $user = $this->client->call(Client::METHOD_DELETE, '/users/' . $data['userId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($user['headers']['status-code'], 404); + + return $data; + } + // TODO add test for session delete // TODO add test for all sessions delete } \ No newline at end of file