diff --git a/app/config/collections.php b/app/config/collections.php index dbff59847..21d1b9117 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1065,9 +1065,9 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, - 'default' => [], - 'array' => true, - 'filters' => ['json'], + 'default' => null, + 'array' => false, + 'filters' => ['subQueryTokens'], ], [ '$id' => 'memberships', @@ -1128,6 +1128,89 @@ $collections = [ ], ], + 'tokens' => [ + '$collection' => Database::METADATA, + '$id' => 'tokens', + 'name' => 'Tokens', + 'attributes' => [ + [ + '$id' => 'userId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'type', + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'secret', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 512, // https://www.tutorialspoint.com/how-long-is-the-sha256-hash-in-mysql (512 for encryption) + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['encrypt'], + ], + [ + '$id' => 'expire', + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'userAgent', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'ip', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 45, // https://stackoverflow.com/a/166157/2299554 + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ] + ], + 'indexes' => [ + [ + '$id' => '_key_user', + 'type' => Database::INDEX_KEY, + 'attributes' => ['userId'], + 'lengths' => [Database::LENGTH_KEY], + 'orders' => [Database::ORDER_ASC], + ], + ], + ], + 'sessions' => [ '$collection' => Database::METADATA, '$id' => 'sessions', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 31bf7bf42..d507680ca 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -105,7 +105,7 @@ App::post('/v1/account') 'name' => $name, 'prefs' => new \stdClass(), 'sessions' => [], - 'tokens' => [], + 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), 'deleted' => false @@ -506,7 +506,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'name' => $name, 'prefs' => new \stdClass(), 'sessions' => [], - 'tokens' => [], + 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), 'deleted' => false @@ -680,7 +680,7 @@ App::post('/v1/account/sessions/magic-url') 'reset' => false, 'prefs' => new \stdClass(), 'sessions' => [], - 'tokens' => [], + 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email]), 'deleted' => false @@ -706,13 +706,12 @@ App::post('/v1/account/sessions/magic-url') Authorization::setRole('user:'.$user->getId()); - $user->setAttribute('tokens', $token, Document::SET_TYPE_APPEND); + $token = $dbForProject->createDocument('tokens', $token + ->setAttribute('$read', ['user:'.$user->getId()]) + ->setAttribute('$write', ['user:'.$user->getId()]) + ); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); - - if (false === $user) { - throw new Exception('Failed to save user to DB', 500, Exception::GENERAL_SERVER_ERROR); - } + $dbForProject->deleteCachedDocument('users', $user->getId()); if(empty($url)) { $url = $request->getProtocol().'://'.$request->getHostname().'/auth/magic-url'; @@ -786,7 +785,7 @@ App::put('/v1/account/sessions/magic-url') /** @var MaxMind\Db\Reader $geodb */ /** @var Appwrite\Event\Event $audits */ - $user = $dbForProject->getDocument('users', $userId); + $user = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId)); if ($user->isEmpty() || $user->getAttribute('deleted')) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); @@ -825,24 +824,14 @@ App::put('/v1/account/sessions/magic-url') ->setAttribute('$write', ['user:' . $user->getId()]) ); - $tokens = $user->getAttribute('tokens', []); - /** * We act like we're updating and validating * the recovery token but actually we don't need it anymore. */ - foreach ($tokens as $key => $singleToken) { - if ($token === $singleToken->getId()) { - unset($tokens[$key]); - } - } + $dbForProject->deleteDocument('tokens', $token); + $dbForProject->deleteCachedDocument('users', $user->getId()); - $user - ->setAttribute('sessions', $session, Document::SET_TYPE_APPEND) - ->setAttribute('tokens', $tokens); - - - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('sessions', $session, Document::SET_TYPE_APPEND)); if (false === $user) { throw new Exception('Failed saving user to DB', 500, Exception::GENERAL_SERVER_ERROR); @@ -952,7 +941,7 @@ App::post('/v1/account/sessions/anonymous') 'name' => null, 'prefs' => new \stdClass(), 'sessions' => [], - 'tokens' => [], + 'tokens' => null, 'memberships' => null, 'search' => $userId, 'deleted' => false @@ -1906,9 +1895,12 @@ App::post('/v1/account/recovery') Authorization::setRole('user:' . $profile->getId()); - $profile->setAttribute('tokens', $recovery, Document::SET_TYPE_APPEND); + $recovery = $dbForProject->createDocument('tokens', $recovery + ->setAttribute('$read', ['user:'.$profile->getId()]) + ->setAttribute('$write', ['user:'.$profile->getId()]) + ); - $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile); + $dbForProject->deleteCachedDocument('users', $profile->getId()); $url = Template::parseURL($url); $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $profile->getId(), 'secret' => $secret, 'expire' => $expire]); @@ -2003,18 +1995,14 @@ App::put('/v1/account/recovery') ->setAttribute('emailVerification', true) ); + $recoveryDocument = $dbForProject->getDocument('tokens', $recovery); + /** * We act like we're updating and validating * the recovery token but actually we don't need it anymore. */ - foreach ($tokens as $key => $token) { - if ($recovery === $token->getId()) { - $recovery = $token; - unset($tokens[$key]); - } - } - - $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('tokens', $tokens)); + $dbForProject->deleteDocument('tokens', $recovery); + $dbForProject->deleteCachedDocument('users', $profile->getId()); $audits ->setParam('userId', $profile->getId()) @@ -2025,7 +2013,7 @@ App::put('/v1/account/recovery') $usage ->setParam('users.update', 1) ; - $response->dynamic($recovery, Response::MODEL_TOKEN); + $response->dynamic($recoveryDocument, Response::MODEL_TOKEN); }); App::post('/v1/account/verification') @@ -2089,9 +2077,12 @@ App::post('/v1/account/verification') Authorization::setRole('user:' . $user->getId()); - $user->setAttribute('tokens', $verification, Document::SET_TYPE_APPEND); + $verification = $dbForProject->createDocument('tokens', $verification + ->setAttribute('$read', ['user:'.$user->getId()]) + ->setAttribute('$write', ['user:'.$user->getId()]) + ); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->deleteCachedDocument('users', $user->getId()); $url = Template::parseURL($url); $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $verificationSecret, 'expire' => $expire]); @@ -2161,7 +2152,7 @@ App::put('/v1/account/verification') /** @var Appwrite\Event\Event $audits */ /** @var Appwrite\Stats\Stats $usage */ - $profile = $dbForProject->getDocument('users', $userId); + $profile = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId)); if ($profile->isEmpty()) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); @@ -2177,19 +2168,15 @@ App::put('/v1/account/verification') Authorization::setRole('user:' . $profile->getId()); $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true)); + + $verificationDocument = $dbForProject->getDocument('tokens', $verification); /** * We act like we're updating and validating * the verification token but actually we don't need it anymore. */ - foreach ($tokens as $key => $token) { - if ($token->getId() === $verification) { - $verification = $token; - unset($tokens[$key]); - } - } - - $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('tokens', $tokens)); + $dbForProject->deleteDocument('tokens', $verification); + $dbForProject->deleteCachedDocument('users', $profile->getId()); $audits ->setParam('userId', $profile->getId()) @@ -2200,5 +2187,5 @@ App::put('/v1/account/verification') $usage ->setParam('users.update', 1) ; - $response->dynamic($verification, Response::MODEL_TOKEN); + $response->dynamic($verificationDocument, Response::MODEL_TOKEN); }); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 2ca54b7f7..f41857680 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -320,7 +320,7 @@ App::post('/v1/teams/:teamId/memberships') 'name' => $name, 'prefs' => new \stdClass(), 'sessions' => [], - 'tokens' => [], + 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), 'deleted' => false diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index bfb7d4d17..b77303ff2 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -64,7 +64,7 @@ App::post('/v1/users') 'name' => $name, 'prefs' => new \stdClass(), 'sessions' => [], - 'tokens' => [], + 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), 'deleted' => false @@ -739,7 +739,7 @@ App::delete('/v1/users/:userId') ->setAttribute("email", null) ->setAttribute("password", null) ->setAttribute("deleted", true) - ->setAttribute("tokens", []) + ->setAttribute("tokens", null) ->setAttribute("search", null) ; diff --git a/app/init.php b/app/init.php index cac7a9701..786284d49 100644 --- a/app/init.php +++ b/app/init.php @@ -301,6 +301,18 @@ Database::addFilter('subQueryWebhooks', } ); +Database::addFilter('subQueryTokens', + function($value) { + return null; + }, + function($value, Document $document, Database $database) { + return Authorization::skip(fn() => $database + ->find('tokens', [ + new Query('userId', Query::TYPE_EQUAL, [$document->getId()]) + ], $database->getIndexLimit(), 0, [])); + } +); + Database::addFilter('subQueryMemberships', function($value) { return null; diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 6ec1acc7c..892d417d9 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -230,6 +230,11 @@ class DeletesV1 extends Worker } } }); + + // Delete tokens + $this->deleteByGroup('tokens', [ + new Query('userId', Query::TYPE_EQUAL, [$userId]) + ], $this->getProjectDB($projectId)); } /**