From bb0db796f760b91c91b779ffef5947a9a9526c2a Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 16 May 2022 11:58:17 +0200 Subject: [PATCH] feat: account update status --- app/config/collections.php | 22 +------- app/controllers/api/account.php | 71 ++++++++------------------ app/controllers/api/teams.php | 3 +- app/controllers/api/users.php | 60 +++++++++------------- app/workers/deletes.php | 42 +++++++-------- src/Appwrite/Migration/Version/V13.php | 9 ++++ 6 files changed, 79 insertions(+), 128 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index 4b6a1c877c..0e2a558210 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1090,18 +1090,7 @@ $collections = [ 'default' => null, 'array' => false, 'filters' => [], - ], - [ - '$id' => 'deleted', - 'type' => Database::VAR_BOOLEAN, - 'format' => '', - 'size' => 0, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], + ] ], 'indexes' => [ [ @@ -1117,14 +1106,7 @@ $collections = [ 'attributes' => ['search'], 'lengths' => [], 'orders' => [], - ], - [ - '$id' => '_key_deleted_email', - 'type' => Database::INDEX_KEY, - 'attributes' => ['deleted', 'email'], - 'lengths' => [0, 320], - 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC], - ], + ] ], ], diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index e8cf064833..b73e90063a 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -81,9 +81,7 @@ App::post('/v1/account') $limit = $project->getAttribute('auths', [])['limit'] ?? 0; if ($limit !== 0) { - $total = $dbForProject->count('users', [ - new Query('deleted', Query::TYPE_EQUAL, [false]), - ], APP_LIMIT_USERS); + $total = $dbForProject->count('users', max: APP_LIMIT_USERS); if ($total >= $limit) { throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED); @@ -108,8 +106,7 @@ App::post('/v1/account') 'sessions' => null, 'tokens' => null, 'memberships' => null, - 'search' => implode(' ', [$userId, $email, $name]), - 'deleted' => false + 'search' => implode(' ', [$userId, $email, $name]) ]))); } catch (Duplicate $th) { throw new Exception('Account already exists', 409, Exception::USER_ALREADY_EXISTS); @@ -170,7 +167,6 @@ App::post('/v1/account/sessions') $protocol = $request->getProtocol(); $profile = $dbForProject->findOne('users', [ - new Query('deleted', Query::TYPE_EQUAL, [false]), new Query('email', Query::TYPE_EQUAL, [$email])] ); @@ -480,7 +476,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') if ($isVerified === true) { // Get user by email address $user = $dbForProject->findOne('users', [ - new Query('deleted', Query::TYPE_EQUAL, [false]), new Query('email', Query::TYPE_EQUAL, [$email])] ); } @@ -489,7 +484,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $limit = $project->getAttribute('auths', [])['limit'] ?? 0; if ($limit !== 0) { - $total = $dbForProject->count('users', [new Query('deleted', Query::TYPE_EQUAL, [false])], APP_LIMIT_USERS); + $total = $dbForProject->count('users', max: APP_LIMIT_USERS); if ($total >= $limit) { throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED); @@ -514,8 +509,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'sessions' => null, 'tokens' => null, 'memberships' => null, - 'search' => implode(' ', [$userId, $email, $name]), - 'deleted' => false + 'search' => implode(' ', [$userId, $email, $name]) ]))); } catch (Duplicate $th) { throw new Exception('Account already exists', 409, Exception::USER_ALREADY_EXISTS); @@ -663,9 +657,7 @@ App::post('/v1/account/sessions/magic-url') $limit = $project->getAttribute('auths', [])['limit'] ?? 0; if ($limit !== 0) { - $total = $dbForProject->count('users', [ - new Query('deleted', Query::TYPE_EQUAL, [false]), - ], APP_LIMIT_USERS); + $total = $dbForProject->count('users', max: APP_LIMIT_USERS); if ($total >= $limit) { throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED); @@ -689,8 +681,7 @@ App::post('/v1/account/sessions/magic-url') 'sessions' => null, 'tokens' => null, 'memberships' => null, - 'search' => implode(' ', [$userId, $email]), - 'deleted' => false + 'search' => implode(' ', [$userId, $email]) ]))); } @@ -790,7 +781,7 @@ App::put('/v1/account/sessions/magic-url') $user = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId)); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -925,9 +916,7 @@ App::post('/v1/account/sessions/anonymous') $limit = $project->getAttribute('auths', [])['limit'] ?? 0; if ($limit !== 0) { - $total = $dbForProject->count('users', [ - new Query('deleted', Query::TYPE_EQUAL, [false]), - ], APP_LIMIT_USERS); + $total = $dbForProject->count('users', max: APP_LIMIT_USERS); if ($total >= $limit) { throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED); @@ -951,8 +940,7 @@ App::post('/v1/account/sessions/anonymous') 'sessions' => null, 'tokens' => null, 'memberships' => null, - 'search' => $userId, - 'deleted' => false + 'search' => $userId ]))); // Create session token @@ -1469,17 +1457,18 @@ App::patch('/v1/account/prefs') $response->dynamic($user, Response::MODEL_USER); }); -App::delete('/v1/account') - ->desc('Delete Account') +App::patch('/v1/account/status') + ->desc('Update Account Status') ->groups(['api', 'account']) - ->label('event', 'users.[userId].delete') + ->label('event', 'users.[userId].update.status') ->label('scope', 'account') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') - ->label('sdk.method', 'delete') - ->label('sdk.description', '/docs/references/account/delete.md') - ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) - ->label('sdk.response.model', Response::MODEL_NONE) + ->label('sdk.method', 'updateStatus') + ->label('sdk.description', '/docs/references/account/update-status.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) ->inject('request') ->inject('response') ->inject('user') @@ -1496,28 +1485,15 @@ App::delete('/v1/account') /** @var Appwrite\Event\Event $events */ /** @var Appwrite\Stats\Stats $usage */ - $protocol = $request->getProtocol(); $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', false)); - // TODO Seems to be related to users.php/App::delete('/v1/users/:userId'). Can we share code between these two? Do todos below apply to users.php? - - // TODO delete all tokens or only current session? - // TODO delete all user data according to GDPR. Make sure everything is backed up and backups are deleted later - /** - * Data to delete - * * Tokens - * * Memberships - */ - $audits ->setResource('user/' . $user->getId()) - ->setPayload($response->output($user, Response::MODEL_USER)) - ; + ->setPayload($response->output($user, Response::MODEL_USER)); $events ->setParam('userId', $user->getId()) - ->setPayload($response->output($user, Response::MODEL_USER)) - ; + ->setPayload($response->output($user, Response::MODEL_USER)); if (!Config::getParam('domainVerification')) { $response->addHeader('X-Fallback-Cookies', \json_encode([])); @@ -1525,11 +1501,7 @@ App::delete('/v1/account') $usage->setParam('users.delete', 1); - $response - ->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) - ->noContent() - ; + $response->dynamic($user, Response::MODEL_USER); }); App::delete('/v1/account/sessions/:sessionId') @@ -1839,7 +1811,6 @@ App::post('/v1/account/recovery') $email = \strtolower($email); $profile = $dbForProject->findOne('users', [ - new Query('deleted', Query::TYPE_EQUAL, [false]), new Query('email', Query::TYPE_EQUAL, [$email]) ]); @@ -1942,7 +1913,7 @@ App::put('/v1/account/recovery') $profile = $dbForProject->getDocument('users', $userId); - if ($profile->isEmpty() || $profile->getAttribute('deleted')) { + if ($profile->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index f290c2e7a0..e18e87886d 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -348,8 +348,7 @@ App::post('/v1/teams/:teamId/memberships') 'sessions' => null, 'tokens' => null, 'memberships' => null, - 'search' => implode(' ', [$userId, $email, $name]), - 'deleted' => false + 'search' => implode(' ', [$userId, $email, $name]) ]))); } catch (Duplicate $th) { throw new Exception('Account already exists', 409, Exception::USER_ALREADY_EXISTS); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 1eff6f9da0..29284f6aa8 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -68,8 +68,7 @@ App::post('/v1/users') 'sessions' => null, 'tokens' => null, 'memberships' => null, - 'search' => implode(' ', [$userId, $email, $name]), - 'deleted' => false + 'search' => implode(' ', [$userId, $email, $name]) ])); } catch (Duplicate $th) { throw new Exception('Account already exists', 409, Exception::USER_ALREADY_EXISTS); @@ -120,9 +119,7 @@ App::get('/v1/users') } } - $queries = [ - new Query('deleted', Query::TYPE_EQUAL, [false]) - ]; + $queries = []; if (!empty($search)) { $queries[] = new Query('search', Query::TYPE_SEARCH, [$search]); @@ -160,7 +157,7 @@ App::get('/v1/users/:userId') $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -192,7 +189,7 @@ App::get('/v1/users/:userId/prefs') $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -228,7 +225,7 @@ App::get('/v1/users/:userId/sessions') $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -274,7 +271,7 @@ App::get('/v1/users/:userId/memberships') $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -324,7 +321,7 @@ App::get('/v1/users/:userId/logs') $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -409,7 +406,7 @@ App::patch('/v1/users/:userId/status') $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -452,7 +449,7 @@ App::patch('/v1/users/:userId/verification') $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -495,7 +492,7 @@ App::patch('/v1/users/:userId/name') $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -543,7 +540,7 @@ App::patch('/v1/users/:userId/password') $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -590,7 +587,7 @@ App::patch('/v1/users/:userId/email') $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -650,7 +647,7 @@ App::patch('/v1/users/:userId/prefs') $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -684,15 +681,17 @@ App::delete('/v1/users/:userId/sessions/:sessionId') ->inject('dbForProject') ->inject('events') ->inject('usage') - ->action(function ($userId, $sessionId, $response, $dbForProject, $events, $usage) { + ->inject('deletes') + ->action(function ($userId, $sessionId, $response, $dbForProject, $events, $usage, $deletes) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForProject */ /** @var Appwrite\Event\Event $events */ /** @var Appwrite\Stats\Stats $usage */ + /** @var Appwrite\Event\Delete $deletes */ $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -716,6 +715,11 @@ App::delete('/v1/users/:userId/sessions/:sessionId') ->setParam('sessionId', $sessionId) ; + $deletes + ->setType(DELETE_TYPE_USERS) + ->setDocument($user) + ; + $response->noContent(); }); @@ -743,7 +747,7 @@ App::delete('/v1/users/:userId/sessions') $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } @@ -795,28 +799,14 @@ App::delete('/v1/users/:userId') $user = $dbForProject->getDocument('users', $userId); - if ($user->isEmpty() || $user->getAttribute('deleted')) { + if ($user->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } - /** - * DO NOT DELETE THE USER RECORD ITSELF. - * WE RETAIN THE USER RECORD TO RESERVE THE USER ID AND ENSURE THAT THE USER ID IS NOT REUSED. - */ - // clone user object to send to workers $clone = clone $user; - $user - ->setAttribute("name", null) - ->setAttribute("email", null) - ->setAttribute("password", null) - ->setAttribute("deleted", true) - ->setAttribute("tokens", null) - ->setAttribute("search", null) - ; - - $dbForProject->updateDocument('users', $userId, $user); + $dbForProject->deleteDocument('users', $userId); $deletes ->setType(DELETE_TYPE_DOCUMENT) diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 295a82c713..0f9d76f81e 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -31,7 +31,8 @@ class DeletesV1 extends Worker */ protected $consoleDB = null; - public function getName(): string { + public function getName(): string + { return "deletes"; } @@ -202,11 +203,6 @@ class DeletesV1 extends Worker */ protected function deleteUser(Document $document, string $projectId): void { - /** - * DO NOT DELETE THE USER RECORD ITSELF. - * WE RETAIN THE USER RECORD TO RESERVE THE USER ID AND ENSURE THAT THE USER ID IS NOT REUSED. - */ - $userId = $document->getId(); // Delete all sessions of this user from the sessions table and update the sessions field of the user record @@ -225,9 +221,14 @@ class DeletesV1 extends Worker $teamId = $document->getAttribute('teamId'); $team = $this->getProjectDB($projectId)->getDocument('teams', $teamId); if (!$team->isEmpty()) { - $team = $this->getProjectDB($projectId)->updateDocument('teams', $teamId, new Document(\array_merge($team->getArrayCopy(), [ - 'total' => \max($team->getAttribute('total', 0) - 1, 0), // Ensure that total >= 0 - ]))); + $team = $this + ->getProjectDB($projectId) + ->updateDocument( + 'teams', + $teamId, + // Ensure that total >= 0 + $team->setAttribute('total', \max($team->getAttribute('total', 0) - 1, 0)) + ); } } }); @@ -348,7 +349,7 @@ class DeletesV1 extends Worker */ Console::info("Deleting builds for function " . $functionId); $storageBuilds = new Local(APP_STORAGE_BUILDS . '/app-' . $projectId); - foreach ($deploymentIds as $deploymentId) { + foreach ($deploymentIds as $deploymentId) { $this->deleteByGroup('builds', [ new Query('deploymentId', Query::TYPE_EQUAL, [$deploymentId]) ], $dbForProject, function (Document $document) use ($storageBuilds, $deploymentId) { @@ -362,7 +363,7 @@ class DeletesV1 extends Worker /** * Delete Executions - */ + */ Console::info("Deleting executions for function " . $functionId); $this->deleteByGroup('executions', [ new Query('functionId', Query::TYPE_EQUAL, [$functionId]) @@ -380,7 +381,6 @@ class DeletesV1 extends Worker Console::error($th->getMessage()); } } - } /** @@ -474,7 +474,7 @@ class DeletesV1 extends Worker $chunk++; /** @var string[] $projectIds */ - $projectIds = array_map(fn(Document $project) => $project->getId(), $projects); + $projectIds = array_map(fn (Document $project) => $project->getId(), $projects); $sum = count($projects); @@ -533,21 +533,21 @@ class DeletesV1 extends Worker $consoleDB = $this->getConsoleDB(); // If domain has certificate generated - if(isset($document['certificateId'])) { + if (isset($document['certificateId'])) { $domainUsingCertificate = $consoleDB->findOne('domains', [ new Query('certificateId', Query::TYPE_EQUAL, [$document['certificateId']]) ]); - if(!$domainUsingCertificate) { + if (!$domainUsingCertificate) { $mainDomain = App::getEnv('_APP_DOMAIN_TARGET', ''); - if($mainDomain === $document->getAttribute('domain')) { + if ($mainDomain === $document->getAttribute('domain')) { $domainUsingCertificate = $mainDomain; } } // If certificate is still used by some domain, mark we can't delete. // Current domain should not be found, because we only have copy. Original domain is already deleted from database. - if($domainUsingCertificate) { + if ($domainUsingCertificate) { Console::warning("Skipping certificate deletion, because a domain is still using it."); return; } @@ -559,7 +559,7 @@ class DeletesV1 extends Worker if ($domain && $checkTraversal && is_dir($directory)) { // Delete certificate document, so Appwrite is aware of change - if(isset($document['certificateId'])) { + if (isset($document['certificateId'])) { $consoleDB->deleteDocument('certificates', $document['certificateId']); } @@ -577,8 +577,8 @@ class DeletesV1 extends Worker $dbForProject = $this->getProjectDB($projectId); $dbForProject->deleteCollection('bucket_' . $document->getInternalId()); - $device = new Local(APP_STORAGE_UPLOADS.'/app-'.$projectId); - + $device = new Local(APP_STORAGE_UPLOADS . '/app-' . $projectId); + switch (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)) { case Storage::DEVICE_S3: $s3AccessKey = App::getEnv('_APP_STORAGE_S3_ACCESS_KEY', ''); @@ -597,7 +597,7 @@ class DeletesV1 extends Worker $device = new DOSpaces(APP_STORAGE_UPLOADS . '/app-' . $projectId, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl); break; } - + $device->deletePath($document->getId()); } } diff --git a/src/Appwrite/Migration/Version/V13.php b/src/Appwrite/Migration/Version/V13.php index 4109072816..5c5ad08e8e 100644 --- a/src/Appwrite/Migration/Version/V13.php +++ b/src/Appwrite/Migration/Version/V13.php @@ -256,6 +256,15 @@ class V13 extends Migration } break; + + case 'users': + /** + * Remove deleted users. + */ + if ($document->getAttribute('deleted', false) === true) { + $this->projectDB->deleteDocument('users', $document->getId()); + } + break; } return $document;