delete account api

This commit is contained in:
shimon
2024-01-10 21:13:05 +02:00
parent fe42351aea
commit 779024068e
6 changed files with 131 additions and 119 deletions
-1
View File
@@ -103,7 +103,6 @@ RUN chmod +x /usr/local/bin/hamster && \
chmod +x /usr/local/bin/patch-delete-schedule-updated-at-attribute && \
chmod +x /usr/local/bin/patch-recreate-repositories-documents && \
chmod +x /usr/local/bin/patch-delete-project-collections && \
chmod +x /usr/local/bin/patch-delete-console-users && \
chmod +x /usr/local/bin/delete-orphaned-projects && \
chmod +x /usr/local/bin/clear-card-cache && \
chmod +x /usr/local/bin/calc-users-stats && \
+55 -11
View File
@@ -165,6 +165,55 @@ App::post('/v1/account')
->dynamic($user, Response::MODEL_ACCOUNT);
});
App::delete('/v1/account')
->desc('Delete account')
->groups(['api', 'account'])
->label('event', 'users.[userId].delete')
->label('scope', 'account')
->label('audits.event', 'user.delete')
->label('audits.resource', 'user/{request.userId}')
->label('usage.metric', 'users.{scope}.requests.delete')
->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)
->inject('requestTimestamp')
->inject('request')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForDeletes')
->action(function (?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
$clone = clone $user;
$dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->deleteDocument('users', $user->getId()));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($clone);
$queueForEvents
->setParam('userId', $clone->getId())
->setPayload($response->output($clone, Response::MODEL_ACCOUNT));
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
}
$protocol = $request->getProtocol();
$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'))
;
$response->noContent();
});
App::post('/v1/account/sessions/email')
->alias('/v1/account/sessions')
->desc('Create email session')
@@ -2133,20 +2182,15 @@ App::patch('/v1/account/status')
->inject('user')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForDeletes')
->action(function (?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
->action(function (?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
$clone = clone $user;
$user->setAttribute('status', false);
$dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->deleteDocument('users', $user->getId()));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($clone);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
$queueForEvents
->setParam('userId', $clone->getId())
->setPayload($response->output($clone, Response::MODEL_ACCOUNT));
->setParam('userId', $user->getId())
->setPayload($response->output($user, Response::MODEL_ACCOUNT));
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
@@ -2158,7 +2202,7 @@ App::patch('/v1/account/status')
->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
;
$response->dynamic($clone, Response::MODEL_ACCOUNT);
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
App::delete('/v1/account/sessions/:sessionId')
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
php /usr/src/code/app/cli.php patch-delete-console-users $@
-3
View File
@@ -20,7 +20,6 @@ use Appwrite\Platform\Tasks\CalcTierStats;
use Appwrite\Platform\Tasks\Upgrade;
use Appwrite\Platform\Tasks\DeleteOrphanedProjects;
use Appwrite\Platform\Tasks\GetMigrationStats;
use Appwrite\Platform\Tasks\PatchDeleteConsoleUsers;
class Tasks extends Service
{
@@ -44,9 +43,7 @@ class Tasks extends Service
->addAction(Specs::getName(), new Specs())
->addAction(CalcTierStats::getName(), new CalcTierStats())
->addAction(DeleteOrphanedProjects::getName(), new DeleteOrphanedProjects())
->addAction(PatchDeleteConsoleUsers::getName(), new PatchDeleteConsoleUsers())
->addAction(GetMigrationStats::getName(), new GetMigrationStats())
->addAction(PatchDeleteConsoleUsers::getName(), new PatchDeleteConsoleUsers())
;
}
}
@@ -1,99 +0,0 @@
<?php
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Delete;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Platform\Action;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Query;
class PatchDeleteConsoleUsers extends Action
{
public static function getName(): string
{
return 'patch-delete-console-users';
}
public function __construct()
{
$this
->desc('Delete console users patch')
->inject('dbForConsole')
->inject('queueForDeletes')
->callback(fn ($dbForConsole, $queueForDeletes) => $this->action($dbForConsole, $queueForDeletes));
}
public function action(Database $dbForConsole, Delete $queueForDeletes): void
{
Console::info("Starting the patch");
$startTime = microtime(true);
$query = [Query::equal('status', [false])];
$this->foreachDocument($dbForConsole, 'users', $query, function ($user) use ($dbForConsole, $queueForDeletes) {
$clone = clone $user;
try {
$dbForConsole->deleteDocument('users', $user->getId());
$queueForDeletes
->setProject(new Document([
'$id' => ID::custom('console'),
'$internalId' => ID::custom('console')
]))
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($clone)
->trigger();
} catch (\Throwable $th) {
Console::error("Unexpected error occurred with User ID {$clone->getId()}");
Console::error('[Error] Type: ' . get_class($th));
Console::error('[Error] Message: ' . $th->getMessage());
Console::error('[Error] File: ' . $th->getFile());
Console::error('[Error] Line: ' . $th->getLine());
}
});
$endTime = microtime(true);
$timeTaken = $endTime - $startTime;
$hours = (int)($timeTaken / 3600);
$timeTaken -= $hours * 3600;
$minutes = (int)($timeTaken / 60);
$timeTaken -= $minutes * 60;
$seconds = (int)$timeTaken;
$milliseconds = ($timeTaken - $seconds) * 1000;
Console::info("Delete console users patch completed in $hours h, $minutes m, $seconds s, $milliseconds mis ( total $timeTaken milliseconds)");
}
protected function foreachDocument(Database $database, string $collection, array $queries = [], callable $callback = null): void
{
$limit = 1000;
$results = [];
$sum = $limit;
$latestDocument = null;
while ($sum === $limit) {
$newQueries = $queries;
if ($latestDocument != null) {
array_unshift($newQueries, Query::cursorAfter($latestDocument));
}
$newQueries[] = Query::limit($limit);
$results = $database->find($collection, $newQueries);
if (empty($results)) {
return;
}
$sum = count($results);
foreach ($results as $document) {
if (is_callable($callback)) {
$callback($document);
}
}
$latestDocument = $results[array_key_last($results)];
}
}
}
@@ -90,6 +90,82 @@ class AccountCustomClientTest extends Scope
return [];
}
public function testDeleteAccount()
{
$email = uniqid() . 'user@localhost.test';
$password = 'password';
$name = 'User Name (blocked)';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
]);
$response['body']['$id'];
$this->assertEquals($response['headers']['status-code'], 201);
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', 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);
$response['body']['$id'];
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$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);
$response = $this->client->call(Client::METHOD_DELETE, '/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'], 204);
$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'], 401);
$response = $this->client->call(Client::METHOD_DELETE, '/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'], 401);
}
public function testBlockedAccount(): array
{
$email = uniqid() . 'user@localhost.test';
@@ -221,8 +297,6 @@ class AccountCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
], [
'status' => false,
]);
$this->assertEquals($response['headers']['status-code'], 200);