diff --git a/.env b/.env index ae597d5bc7..46b07399a8 100644 --- a/.env +++ b/.env @@ -112,6 +112,7 @@ _APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000 _APP_MAINTENANCE_RETENTION_SCHEDULES=86400 _APP_USAGE_STATS=enabled _APP_LOGGING_CONFIG= +_APP_LOGGING_CONFIG_REALTIME= _APP_GRAPHQL_MAX_BATCH_SIZE=10 _APP_GRAPHQL_MAX_COMPLEXITY=250 _APP_GRAPHQL_MAX_DEPTH=4 diff --git a/Dockerfile b/Dockerfile index 0ab9ca163d..ae08d18641 100755 --- a/Dockerfile +++ b/Dockerfile @@ -24,9 +24,9 @@ ENV _APP_VERSION=$VERSION \ _APP_HOME=https://appwrite.io RUN \ - if [ "$DEBUG" == "true" ]; then \ + if [ "$DEBUG" == "true" ]; then \ apk add boost boost-dev; \ - fi + fi WORKDIR /usr/src/code diff --git a/app/assets/dbip/dbip-country-lite-2024-09.mmdb b/app/assets/dbip/dbip-country-lite-2024-09.mmdb deleted file mode 100644 index 43d6bcdeea..0000000000 Binary files a/app/assets/dbip/dbip-country-lite-2024-09.mmdb and /dev/null differ diff --git a/app/assets/dbip/dbip-country-lite-2025-12.mmdb b/app/assets/dbip/dbip-country-lite-2025-12.mmdb new file mode 100644 index 0000000000..4ecabbf735 Binary files /dev/null and b/app/assets/dbip/dbip-country-lite-2025-12.mmdb differ diff --git a/app/cli.php b/app/cli.php index 71b6464cb9..73134887ea 100644 --- a/app/cli.php +++ b/app/cli.php @@ -9,6 +9,7 @@ use Appwrite\Event\StatsResources; use Appwrite\Event\StatsUsage; use Appwrite\Platform\Appwrite; use Appwrite\Runtimes\Runtimes; +use Appwrite\Utopia\Database\Documents\User; use Executor\Executor; use Swoole\Runtime; use Swoole\Timer; @@ -76,6 +77,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) { ->setNamespace('_console') ->setMetadata('host', \gethostname()) ->setMetadata('project', 'console'); + $dbForPlatform->setDocumentType('users', User::class); // Ensure tables exist $collections = Config::getParam('collections', [])['console']; diff --git a/app/config/collections/common.php b/app/config/collections/common.php index f706b9a088..36ef830c63 100644 --- a/app/config/collections/common.php +++ b/app/config/collections/common.php @@ -1,6 +1,6 @@ 256, 'signed' => true, 'required' => false, - 'default' => Auth::DEFAULT_ALGO, + 'default' => (new Argon2())->getName(), 'array' => false, 'filters' => [], ], @@ -184,7 +184,7 @@ return [ 'size' => 65535, 'signed' => true, 'required' => false, - 'default' => Auth::DEFAULT_ALGO_OPTIONS, + 'default' => (new Argon2())->getOptions(), 'array' => false, 'filters' => ['json'], ], diff --git a/app/config/console.php b/app/config/console.php index f8f68a8039..5c4bf87614 100644 --- a/app/config/console.php +++ b/app/config/console.php @@ -4,7 +4,6 @@ * Initializes console project document. */ -use Appwrite\Auth\Auth; use Appwrite\Network\Platform; use Utopia\Database\Helpers\ID; use Utopia\System\System; @@ -38,7 +37,7 @@ $console = [ 'mockNumbers' => [], 'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled', 'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user - 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds + 'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds 'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled', 'invalidateSessions' => true ], diff --git a/app/config/frameworks.php b/app/config/frameworks.php index 47e26ac91e..d224a6e170 100644 --- a/app/config/frameworks.php +++ b/app/config/frameworks.php @@ -215,7 +215,7 @@ return [ 'key' => 'ssr', 'buildCommand' => 'npm run build', 'installCommand' => 'npm install', - 'outputDirectory' => './dist', + 'outputDirectory' => './.output', 'startCommand' => 'bash helpers/tanstack-start/server.sh', ], 'static' => [ diff --git a/app/config/roles.php b/app/config/roles.php index 0f0945a2b4..3bf2297550 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -1,6 +1,6 @@ [ + User::ROLE_GUESTS => [ 'label' => 'Guests', 'scopes' => [ 'global', @@ -112,23 +112,23 @@ return [ 'execution.write', ], ], - Auth::USER_ROLE_USERS => [ + User::ROLE_USERS => [ 'label' => 'Users', 'scopes' => \array_merge($member), ], - Auth::USER_ROLE_ADMIN => [ + User::ROLE_ADMIN => [ 'label' => 'Admin', 'scopes' => \array_merge($admins), ], - Auth::USER_ROLE_DEVELOPER => [ + User::ROLE_DEVELOPER => [ 'label' => 'Developer', 'scopes' => \array_merge($admins), ], - Auth::USER_ROLE_OWNER => [ + User::ROLE_OWNER => [ 'label' => 'Owner', 'scopes' => \array_merge($member, $admins), ], - Auth::USER_ROLE_APPS => [ + User::ROLE_APPS => [ 'label' => 'Applications', 'scopes' => ['global', 'health.read', 'graphql'], ], diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 3daf69a3f3..80a8129494 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1,10 +1,7 @@ $dbForProject->getDocument('users', $userId)); if ($userFromRequest->isEmpty()) { throw new Exception(Exception::USER_INVALID_TOKEN); } - $verifiedToken = Auth::tokenVerify($userFromRequest->getAttribute('tokens', []), null, $secret); + $verifiedToken = $userFromRequest->tokenVerify(null, $secret, $proofForToken) + ?: $userFromRequest->tokenVerify(null, $secret, $proofForCode); if (!$verifiedToken) { throw new Exception(Exception::USER_INVALID_TOKEN); @@ -209,27 +212,36 @@ $createSession = function (string $userId, string $secret, Request $request, Res $user->setAttributes($userFromRequest->getArrayCopy()); - $duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); - $sessionSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION); + $sessionSecret = $proofForToken->generate(); $factor = (match ($verifiedToken->getAttribute('type')) { - Auth::TOKEN_TYPE_MAGIC_URL, - Auth::TOKEN_TYPE_OAUTH2, - Auth::TOKEN_TYPE_EMAIL => Type::EMAIL, - Auth::TOKEN_TYPE_PHONE => Type::PHONE, - Auth::TOKEN_TYPE_GENERIC => 'token', + TOKEN_TYPE_MAGIC_URL, + TOKEN_TYPE_OAUTH2, + TOKEN_TYPE_EMAIL => Type::EMAIL, + TOKEN_TYPE_PHONE => Type::PHONE, + TOKEN_TYPE_GENERIC => 'token', default => throw new Exception(Exception::USER_INVALID_TOKEN) }); + $provider = match ($verifiedToken->getAttribute('type')) { + TOKEN_TYPE_VERIFICATION, + TOKEN_TYPE_RECOVERY, + TOKEN_TYPE_INVITE => SESSION_PROVIDER_EMAIL, + TOKEN_TYPE_MAGIC_URL => SESSION_PROVIDER_MAGIC_URL, + TOKEN_TYPE_PHONE => SESSION_PROVIDER_PHONE, + TOKEN_TYPE_OAUTH2 => SESSION_PROVIDER_OAUTH2, + default => SESSION_PROVIDER_TOKEN, + }; $session = new Document(array_merge( [ '$id' => ID::unique(), 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), - 'provider' => Auth::getSessionProviderByTokenType($verifiedToken->getAttribute('type')), - 'secret' => Auth::hash($sessionSecret), // One way hash encryption to protect DB leak + 'provider' => $provider, + 'secret' => $proofForToken->hash($sessionSecret), // One way hash encryption to protect DB leak 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), 'factors' => [$factor], @@ -254,11 +266,11 @@ $createSession = function (string $userId, string $secret, Request $request, Res $dbForProject->purgeCachedDocument('users', $user->getId()); // Magic URL + Email OTP - if ($verifiedToken->getAttribute('type') === Auth::TOKEN_TYPE_MAGIC_URL || $verifiedToken->getAttribute('type') === Auth::TOKEN_TYPE_EMAIL) { + if ($verifiedToken->getAttribute('type') === TOKEN_TYPE_MAGIC_URL || $verifiedToken->getAttribute('type') === TOKEN_TYPE_EMAIL) { $user->setAttribute('emailVerification', true); } - if ($verifiedToken->getAttribute('type') === Auth::TOKEN_TYPE_PHONE) { + if ($verifiedToken->getAttribute('type') === TOKEN_TYPE_PHONE) { $user->setAttribute('phoneVerification', true); } @@ -269,8 +281,8 @@ $createSession = function (string $userId, string $secret, Request $request, Res } $isAllowedTokenType = match ($verifiedToken->getAttribute('type')) { - Auth::TOKEN_TYPE_MAGIC_URL, - Auth::TOKEN_TYPE_EMAIL => false, + TOKEN_TYPE_MAGIC_URL, + TOKEN_TYPE_EMAIL => false, default => true }; @@ -290,16 +302,21 @@ $createSession = function (string $userId, string $secret, Request $request, Res ->setParam('userId', $user->getId()) ->setParam('sessionId', $session->getId()); + $encoded = $store + ->setProperty('id', $user->getId()) + ->setProperty('secret', $sessionSecret) + ->encode(); + if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $sessionSecret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $protocol = $request->getProtocol(); $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED); $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); @@ -308,7 +325,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res ->setAttribute('current', true) ->setAttribute('countryName', $countryName) ->setAttribute('expire', $expire) - ->setAttribute('secret', Auth::encodeSession($user->getId(), $sessionSecret)) + ->setAttribute('secret', $encoded) ; $response->dynamic($session, Response::MODEL_SESSION); @@ -394,7 +411,8 @@ App::post('/v1/account') $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]); $passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0; - $password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); + $proof = new ProofsPassword(); + $hash = $proof->hash($password); try { $emailCanonical = new Email($email); @@ -414,11 +432,11 @@ App::post('/v1/account') 'email' => $email, 'emailVerification' => false, 'status' => true, - 'password' => $password, - 'passwordHistory' => $passwordHistory > 0 ? [$password] : [], + 'password' => $hash, + 'passwordHistory' => $passwordHistory > 0 ? [$hash] : [], 'passwordUpdate' => DateTime::now(), - 'hash' => Auth::DEFAULT_ALGO, - 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, + 'hash' => $proof->getHash()->getName(), + 'hashOptions' => $proof->getHash()->getOptions(), 'registration' => DateTime::now(), 'reset' => false, 'name' => $name, @@ -579,12 +597,13 @@ App::get('/v1/account/sessions') ->inject('response') ->inject('user') ->inject('locale') - ->inject('project') - ->action(function (Response $response, Document $user, Locale $locale, Document $project) { + ->inject('store') + ->inject('proofForToken') + ->action(function (Response $response, User $user, Locale $locale, Store $store, ProofsToken $proofForToken) { $sessions = $user->getAttribute('sessions', []); - $current = Auth::sessionVerify($sessions, Auth::$secret); + $current = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken); foreach ($sessions as $key => $session) {/** @var Document $session */ $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); @@ -631,7 +650,9 @@ App::delete('/v1/account/sessions') ->inject('locale') ->inject('queueForEvents') ->inject('queueForDeletes') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes) { + ->inject('store') + ->inject('proofForToken') + ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) { $protocol = $request->getProtocol(); $sessions = $user->getAttribute('sessions', []); @@ -647,13 +668,13 @@ App::delete('/v1/account/sessions') ->setAttribute('current', false) ->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'))); - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { + if ($proofForToken->verify($store->getProperty('secret', ''), $session->getAttribute('secret'))) { $session->setAttribute('current', true); // If current session delete the cookies too $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')); + ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); // Use current session for events. $queueForEvents @@ -697,12 +718,13 @@ App::get('/v1/account/sessions/:sessionId') ->inject('response') ->inject('user') ->inject('locale') - ->inject('project') - ->action(function (?string $sessionId, Response $response, Document $user, Locale $locale, Document $project) { + ->inject('store') + ->inject('proofForToken') + ->action(function (?string $sessionId, Response $response, User $user, Locale $locale, Store $store, ProofsToken $proofForToken) { $sessions = $user->getAttribute('sessions', []); $sessionId = ($sessionId === 'current') - ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) + ? $user->sessionVerify($store->getProperty('secret', ''), $proofForToken) : $sessionId; foreach ($sessions as $session) {/** @var Document $session */ @@ -710,7 +732,7 @@ App::get('/v1/account/sessions/:sessionId') $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); $session - ->setAttribute('current', ($session->getAttribute('secret') == Auth::hash(Auth::$secret))) + ->setAttribute('current', ($proofForToken->verify($store->getProperty('secret', ''), $session->getAttribute('secret')))) ->setAttribute('countryName', $countryName) ->setAttribute('secret', $session->getAttribute('secret', '')) ; @@ -753,12 +775,13 @@ App::delete('/v1/account/sessions/:sessionId') ->inject('locale') ->inject('queueForEvents') ->inject('queueForDeletes') - ->inject('project') - ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Document $project) { + ->inject('store') + ->inject('proofForToken') + ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) { $protocol = $request->getProtocol(); $sessionId = ($sessionId === 'current') - ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) + ? $user->sessionVerify($store->getProperty('secret', ''), $proofForToken) : $sessionId; $sessions = $user->getAttribute('sessions', []); @@ -775,7 +798,7 @@ App::delete('/v1/account/sessions/:sessionId') $session->setAttribute('current', false); - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + if ($proofForToken->verify($store->getProperty('secret', ''), $session->getAttribute('secret'))) { // If current session delete the cookies too $session ->setAttribute('current', true) ->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'))); @@ -785,8 +808,8 @@ App::delete('/v1/account/sessions/:sessionId') } $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')); + ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); } $dbForProject->purgeCachedDocument('users', $user->getId()); @@ -837,10 +860,12 @@ App::patch('/v1/account/sessions/:sessionId') ->inject('dbForProject') ->inject('project') ->inject('queueForEvents') - ->action(function (?string $sessionId, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents) { + ->inject('store') + ->inject('proofForToken') + ->action(function (?string $sessionId, Response $response, User $user, Database $dbForProject, Document $project, Event $queueForEvents, Store $store, ProofsToken $proofForToken) { $sessionId = ($sessionId === 'current') - ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) + ? $user->sessionVerify($store->getProperty('secret', ''), $proofForToken) : $sessionId; $sessions = $user->getAttribute('sessions', []); @@ -857,7 +882,7 @@ App::patch('/v1/account/sessions/:sessionId') } // Extend session - $authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $authDuration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; $session->setAttribute('expire', DateTime::addSeconds(new \DateTime(), $authDuration)); // Refresh OAuth access token @@ -929,7 +954,10 @@ App::post('/v1/account/sessions/email') ->inject('queueForEvents') ->inject('queueForMails') ->inject('hooks') - ->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks) { + ->inject('store') + ->inject('proofForPassword') + ->inject('proofForToken') + ->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) { $email = \strtolower($email); $protocol = $request->getProtocol(); @@ -937,7 +965,9 @@ App::post('/v1/account/sessions/email') Query::equal('email', [$email]), ]); - if ($profile->isEmpty() || empty($profile->getAttribute('passwordUpdate')) || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) { + $userProofForPassword = ProofsPassword::createHash($profile->getAttribute('hash', $proofForPassword->getHash()->getName()), $profile->getAttribute('hashOptions', $proofForPassword->getHash()->getOptions())); + + if ($profile->isEmpty() || empty($profile->getAttribute('passwordUpdate')) || !$userProofForPassword->verify($password, $profile->getAttribute('password'))) { throw new Exception(Exception::USER_INVALID_CREDENTIALS); } @@ -949,18 +979,18 @@ App::post('/v1/account/sessions/email') $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]); - $duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); - $secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION); + $secret = $proofForToken->generate(); $session = new Document(array_merge( [ '$id' => ID::unique(), 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), - 'provider' => Auth::SESSION_PROVIDER_EMAIL, + 'provider' => SESSION_PROVIDER_EMAIL, 'providerUid' => $email, - 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak + 'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), 'factors' => ['password'], @@ -975,11 +1005,12 @@ App::post('/v1/account/sessions/email') Authorization::setRole(Role::user($user->getId())->toString()); // Re-hash if not using recommended algo - if ($user->getAttribute('hash') !== Auth::DEFAULT_ALGO) { + if ($user->getAttribute('hash') !== $proofForPassword->getHash()->getName()) { + $proofForPasswordUpdated = new ProofsPassword(); $user - ->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS)) - ->setAttribute('hash', Auth::DEFAULT_ALGO) - ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS); + ->setAttribute('password', $proofForPasswordUpdated->hash($password)) + ->setAttribute('hash', $proofForPasswordUpdated->getHash()->getName()) + ->setAttribute('hashOptions', $proofForPasswordUpdated->getHash()->getOptions()); $dbForProject->updateDocument('users', $user->getId(), $user); } @@ -991,17 +1022,20 @@ App::post('/v1/account/sessions/email') Permission::delete(Role::user($user->getId())), ])); + $encoded = $store + ->setProperty('id', $user->getId()) + ->setProperty('secret', $secret) + ->encode(); + if (!Config::getParam('domainVerification')) { - $response - ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) - ; + $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED) ; @@ -1010,7 +1044,7 @@ App::post('/v1/account/sessions/email') $session ->setAttribute('current', true) ->setAttribute('countryName', $countryName) - ->setAttribute('secret', Auth::encodeSession($user->getId(), $secret)) + ->setAttribute('secret', $encoded) ; $queueForEvents @@ -1064,7 +1098,10 @@ App::post('/v1/account/sessions/anonymous') ->inject('dbForProject') ->inject('geodb') ->inject('queueForEvents') - ->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents) { + ->inject('store') + ->inject('proofForPassword') + ->inject('proofForToken') + ->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) { $protocol = $request->getProtocol(); if ('console' === $project->getId()) { @@ -1093,8 +1130,8 @@ App::post('/v1/account/sessions/anonymous') 'emailVerification' => false, 'status' => true, 'password' => null, - 'hash' => Auth::DEFAULT_ALGO, - 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, + 'hash' => $proofForPassword->getHash()->getName(), + 'hashOptions' => $proofForPassword->getHash()->getOptions(), 'passwordUpdate' => null, 'registration' => DateTime::now(), 'reset' => false, @@ -1113,18 +1150,18 @@ App::post('/v1/account/sessions/anonymous') $user = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); // Create session token - $duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); - $secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION); + $secret = $proofForToken->generate(); $session = new Document(array_merge( [ '$id' => ID::unique(), 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), - 'provider' => Auth::SESSION_PROVIDER_ANONYMOUS, - 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak + 'provider' => SESSION_PROVIDER_ANONYMOUS, + 'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), 'factors' => ['anonymous'], @@ -1151,15 +1188,20 @@ App::post('/v1/account/sessions/anonymous') ->setParam('sessionId', $session->getId()) ; + $encoded = $store + ->setProperty('id', $user->getId()) + ->setProperty('secret', $secret) + ->encode(); + if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED) ; @@ -1168,7 +1210,7 @@ App::post('/v1/account/sessions/anonymous') $session ->setAttribute('current', true) ->setAttribute('countryName', $countryName) - ->setAttribute('secret', Auth::encodeSession($user->getId(), $secret)) + ->setAttribute('secret', $encoded) ; $response->dynamic($session, Response::MODEL_SESSION); @@ -1209,6 +1251,9 @@ App::post('/v1/account/sessions/token') ->inject('geodb') ->inject('queueForEvents') ->inject('queueForMails') + ->inject('store') + ->inject('proofForToken') + ->inject('proofForCode') ->action($createSession); App::get('/v1/account/sessions/oauth2/:provider') @@ -1401,7 +1446,10 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->inject('dbForProject') ->inject('geodb') ->inject('queueForEvents') - ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, array $platforms, Document $devKey, Document $user, Database $dbForProject, Reader $geodb, Event $queueForEvents) use ($oauthDefaultSuccess) { + ->inject('store') + ->inject('proofForPassword') + ->inject('proofForToken') + ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, array $platforms, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) use ($oauthDefaultSuccess) { $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; $port = $request->getPort(); $callbackBase = $protocol . '://' . $request->getHostname(); @@ -1548,8 +1596,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $sessionUpgrade = true; } - $sessions = $user->getAttribute('sessions', []); - $current = Auth::sessionVerify($sessions, Auth::$secret); + $current = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken); if ($current) { // Delete current session of new one. $currentDocument = $dbForProject->getDocument('sessions', $current); @@ -1636,8 +1683,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'emailVerification' => true, 'status' => true, // Email should already be authenticated by OAuth2 provider 'password' => null, - 'hash' => Auth::DEFAULT_ALGO, - 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, + 'hash' => $proofForPassword->getHash()->getName(), + 'hashOptions' => $proofForPassword->getHash()->getOptions(), 'passwordUpdate' => null, 'registration' => DateTime::now(), 'reset' => false, @@ -1754,18 +1801,20 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $state['success'] = URLParser::parse($state['success']); $query = URLParser::parseQuery($state['success']['query']); - $duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); + $proofForTokenOAuth2 = new ProofsToken(TOKEN_LENGTH_OAUTH2); + $proofForTokenOAuth2->setHash(new Sha()); // If the `token` param is set, we will return the token in the query string if ($state['token']) { - $secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_OAUTH2); + $secret = $proofForTokenOAuth2->generate(); $token = new Document([ '$id' => ID::unique(), 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), - 'type' => Auth::TOKEN_TYPE_OAUTH2, - 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak + 'type' => TOKEN_TYPE_OAUTH2, + 'secret' => $proofForTokenOAuth2->hash($secret), // One way hash encryption to protect DB leak 'expire' => $expire, 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), @@ -1793,7 +1842,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } else { $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); - $secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION); + $secret = $proofForToken->generate(); $session = new Document(array_merge([ '$id' => ID::unique(), @@ -1803,8 +1852,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'providerUid' => $oauth2ID, 'providerAccessToken' => $accessToken, 'providerRefreshToken' => $refreshToken, - 'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int) $accessTokenExpiry), - 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak + 'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry), + 'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), 'factors' => [TYPE::EMAIL, 'oauth2'], // include a special oauth2 factor to bypass MFA checks @@ -1820,8 +1869,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $session->setAttribute('expire', $expire); + $encoded = $store + ->setProperty('id', $user->getId()) + ->setProperty('secret', $secret) + ->encode(); + if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } $queueForEvents @@ -1834,13 +1888,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') if ($state['success']['path'] == $oauthDefaultSuccess) { $query['project'] = $project->getId(); $query['domain'] = Config::getParam('cookieDomain'); - $query['key'] = Auth::$cookieName; - $query['secret'] = Auth::encodeSession($user->getId(), $secret); + $query['key'] = $store->getKey(); + $query['secret'] = $encoded; } $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); + ->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')); } if (isset($sessionUpgrade) && $sessionUpgrade) { @@ -1998,7 +2052,8 @@ App::post('/v1/account/tokens/magic-url') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { + ->inject('proofForPassword') + ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -2050,8 +2105,8 @@ App::post('/v1/account/tokens/magic-url') 'emailVerification' => false, 'status' => true, 'password' => null, - 'hash' => Auth::DEFAULT_ALGO, - 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, + 'hash' => $proofForPassword->getHash()->getName(), + 'hashOptions' => $proofForPassword->getHash()->getOptions(), 'passwordUpdate' => null, 'registration' => DateTime::now(), 'reset' => false, @@ -2074,15 +2129,18 @@ App::post('/v1/account/tokens/magic-url') Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); } - $tokenSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_MAGIC_URL); - $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM)); + $proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL); + $proofForToken->setHash(new Sha()); + + $tokenSecret = $proofForToken->generate(); + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_CONFIRM)); $token = new Document([ '$id' => ID::unique(), 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), - 'type' => Auth::TOKEN_TYPE_MAGIC_URL, - 'secret' => Auth::hash($tokenSecret), // One way hash encryption to protect DB leak + 'type' => TOKEN_TYPE_MAGIC_URL, + 'secret' => $proofForToken->hash($tokenSecret), // One way hash encryption to protect DB leak 'expire' => $expire, 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), @@ -2261,7 +2319,9 @@ App::post('/v1/account/tokens/email') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { + ->inject('proofForPassword') + ->inject('proofForCode') + ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); } @@ -2311,8 +2371,8 @@ App::post('/v1/account/tokens/email') 'emailVerification' => false, 'status' => true, 'password' => null, - 'hash' => Auth::DEFAULT_ALGO, - 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, + 'hash' => $proofForPassword->getHash()->getName(), + 'hashOptions' => $proofForPassword->getHash()->getOptions(), 'passwordUpdate' => null, 'registration' => DateTime::now(), 'reset' => false, @@ -2356,15 +2416,15 @@ App::post('/v1/account/tokens/email') $dbForProject->purgeCachedDocument('users', $user->getId()); } - $tokenSecret = Auth::codeGenerator(6); - $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_OTP)); + $tokenSecret = $proofForCode->generate(); + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_OTP)); $token = new Document([ '$id' => ID::unique(), 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), - 'type' => Auth::TOKEN_TYPE_EMAIL, - 'secret' => Auth::hash($tokenSecret), // One way hash encryption to protect DB leak + 'type' => TOKEN_TYPE_EMAIL, + 'secret' => $proofForCode->hash($tokenSecret), // One way hash encryption to protect DB leak 'expire' => $expire, 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), @@ -2551,7 +2611,13 @@ App::put('/v1/account/sessions/magic-url') ->inject('geodb') ->inject('queueForEvents') ->inject('queueForMails') - ->action($createSession); + ->inject('store') + ->inject('proofForCode') + ->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode) use ($createSession) { + $proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL); + $proofForToken->setHash(new Sha()); + $createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode); + }); App::put('/v1/account/sessions/phone') ->desc('Update phone session') @@ -2592,6 +2658,9 @@ App::put('/v1/account/sessions/phone') ->inject('geodb') ->inject('queueForEvents') ->inject('queueForMails') + ->inject('store') + ->inject('proofForToken') + ->inject('proofForCode') ->action($createSession); App::post('/v1/account/tokens/phone') @@ -2632,7 +2701,9 @@ App::post('/v1/account/tokens/phone') ->inject('timelimit') ->inject('queueForStatsUsage') ->inject('plan') - ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan) { + ->inject('store') + ->inject('proofForCode') + ->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Store $store, ProofsCode $proofForCode) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -2716,15 +2787,15 @@ App::post('/v1/account/tokens/phone') } } - $secret ??= Auth::codeGenerator(); - $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_OTP)); + $secret ??= $proofForCode->generate(); + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_OTP)); $token = new Document([ '$id' => ID::unique(), 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), - 'type' => Auth::TOKEN_TYPE_PHONE, - 'secret' => Auth::hash($secret), + 'type' => TOKEN_TYPE_PHONE, + 'secret' => $proofForCode->hash($secret), 'expire' => $expire, 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), @@ -2799,7 +2870,11 @@ App::post('/v1/account/tokens/phone') ->setPayload($response->output($token, Response::MODEL_TOKEN), sensitive: ['secret']); // Encode secret for clients - $token->setAttribute('secret', Auth::encodeSession($user->getId(), $secret)); + $encoded = $store + ->setProperty('id', $user->getId()) + ->setProperty('secret', $secret) + ->encode(); + $token->setAttribute('secret', $encoded); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -2830,20 +2905,12 @@ App::post('/v1/account/jwts') ->label('abuse-key', 'url:{url},userId:{userId}') ->inject('response') ->inject('user') - ->inject('dbForProject') - ->action(function (Response $response, Document $user, Database $dbForProject) { + ->inject('store') + ->inject('proofForToken') + ->action(function (Response $response, User $user, Store $store, ProofsToken $proofForToken) { + $sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken); - - $sessions = $user->getAttribute('sessions', []); - $current = new Document(); - - foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */ - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too - $current = $session; - } - } - - if ($current->isEmpty()) { + if (!$sessionId) { throw new Exception(Exception::USER_SESSION_NOT_FOUND); } @@ -2854,7 +2921,7 @@ App::post('/v1/account/jwts') ->dynamic(new Document([ 'jwt' => $jwt->encode([ 'userId' => $user->getId(), - 'sessionId' => $current->getId(), + 'sessionId' => $sessionId, ]) ]), Response::MODEL_JWT); }); @@ -2983,12 +3050,11 @@ App::patch('/v1/account/name') contentType: ContentType::JSON )) ->param('name', '', new Text(128), 'User name. Max length: 128 chars.') - ->inject('requestTimestamp') ->inject('response') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $name, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->action(function (string $name, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { $user->setAttribute('name', $name); @@ -3024,25 +3090,29 @@ App::patch('/v1/account/password') ->label('abuse-limit', 10) ->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('oldPassword', '', new Password(), 'Current user password. Must be at least 8 chars.', true) - ->inject('requestTimestamp') ->inject('response') ->inject('user') ->inject('project') ->inject('dbForProject') ->inject('queueForEvents') ->inject('hooks') - ->action(function (string $password, string $oldPassword, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { - + ->inject('store') + ->inject('proofForPassword') + ->inject('proofForToken') + ->action(function (string $password, string $oldPassword, Response $response, User $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) { + $userProofForPassword = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions')); // Check old password only if its an existing user. - if (!empty($user->getAttribute('passwordUpdate')) && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password + if (!empty($user->getAttribute('passwordUpdate')) && !$userProofForPassword->verify($oldPassword, $user->getAttribute('password'))) { // Double check user password throw new Exception(Exception::USER_INVALID_CREDENTIALS); } - $newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); + $newPassword = $proofForPassword->hash($password); $historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0; + $hash = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions')); $history = $user->getAttribute('passwordHistory', []); + if ($historyLimit > 0) { - $validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions')); + $validator = new PasswordHistory($history, $hash); if (!$validator->isValid($password)) { throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED); } @@ -3064,11 +3134,13 @@ App::patch('/v1/account/password') ->setAttribute('password', $newPassword) ->setAttribute('passwordHistory', $history) ->setAttribute('passwordUpdate', DateTime::now()) - ->setAttribute('hash', Auth::DEFAULT_ALGO) - ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS); + ->setAttribute('hash', $proofForPassword->getHash()->getName()) + ->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions()); $sessions = $user->getAttribute('sessions', []); - $current = Auth::sessionVerify($sessions, Auth::$secret); + + $current = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken); + $invalidate = $project->getAttribute('auths', default: [])['invalidateSessions'] ?? false; if ($invalidate && !empty($current)) { foreach ($sessions as $session) { @@ -3116,13 +3188,16 @@ App::patch('/v1/account/email') ->inject('queueForEvents') ->inject('project') ->inject('hooks') - ->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) { + ->inject('proofForPassword') + ->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword) { // passwordUpdate will be empty if the user has never set a password $passwordUpdate = $user->getAttribute('passwordUpdate'); + $userProofForPassword = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions')); + if ( !empty($passwordUpdate) && - !Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions')) + !$userProofForPassword->verify($password, $user->getAttribute('password')) ) { // Double check user password throw new Exception(Exception::USER_INVALID_CREDENTIALS); } @@ -3160,9 +3235,9 @@ App::patch('/v1/account/email') if (empty($passwordUpdate)) { $user - ->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS)) - ->setAttribute('hash', Auth::DEFAULT_ALGO) - ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS) + ->setAttribute('password', $proofForPassword->hash($password)) + ->setAttribute('hash', $proofForPassword->getHash()->getName()) + ->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions()) ->setAttribute('passwordUpdate', DateTime::now()); } @@ -3217,20 +3292,22 @@ App::patch('/v1/account/phone') )) ->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') - ->inject('requestTimestamp') ->inject('response') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') ->inject('project') ->inject('hooks') - ->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) { + ->inject('proofForPassword') + ->action(function (string $phone, string $password, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword) { // passwordUpdate will be empty if the user has never set a password $passwordUpdate = $user->getAttribute('passwordUpdate'); + $userProofForPassword = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions')); + if ( !empty($passwordUpdate) && - !Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions')) + !$userProofForPassword->verify($password, $user->getAttribute('password')) ) { // Double check user password throw new Exception(Exception::USER_INVALID_CREDENTIALS); } @@ -3254,9 +3331,9 @@ App::patch('/v1/account/phone') if (empty($passwordUpdate)) { $user - ->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS)) - ->setAttribute('hash', Auth::DEFAULT_ALGO) - ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS) + ->setAttribute('password', $proofForPassword->hash($password)) + ->setAttribute('hash', $proofForPassword->getHash()->getName()) + ->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions()) ->setAttribute('passwordUpdate', DateTime::now()); } @@ -3339,13 +3416,13 @@ App::patch('/v1/account/status') ], contentType: ContentType::JSON, )) - ->inject('requestTimestamp') ->inject('request') ->inject('response') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { + ->inject('store') + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store) { $user->setAttribute('status', false); @@ -3361,8 +3438,8 @@ App::patch('/v1/account/status') $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')) + ->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ; $response->dynamic($user, Response::MODEL_ACCOUNT); @@ -3402,7 +3479,8 @@ App::post('/v1/account/recovery') ->inject('locale') ->inject('queueForMails') ->inject('queueForEvents') - ->action(function (string $email, string $url, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Mail $queueForMails, Event $queueForEvents) { + ->inject('proofForToken') + ->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); @@ -3424,15 +3502,15 @@ App::post('/v1/account/recovery') throw new Exception(Exception::USER_BLOCKED); } - $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_RECOVERY)); + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_RECOVERY)); - $secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_RECOVERY); + $secret = $proofForToken->generate(); $recovery = new Document([ '$id' => ID::unique(), 'userId' => $profile->getId(), 'userInternalId' => $profile->getSequence(), - 'type' => Auth::TOKEN_TYPE_RECOVERY, - 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak + 'type' => TOKEN_TYPE_RECOVERY, + 'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak 'expire' => $expire, 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), @@ -3580,15 +3658,17 @@ App::put('/v1/account/recovery') ->inject('project') ->inject('queueForEvents') ->inject('hooks') - ->action(function (string $userId, string $secret, string $password, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks) { + ->inject('proofForPassword') + ->inject('proofForToken') + ->action(function (string $userId, string $secret, string $password, Response $response, User $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks, ProofsPassword $proofForPassword, ProofsToken $proofForToken) { + /** @var Appwrite\Utopia\Database\Documents\User $profile */ $profile = $dbForProject->getDocument('users', $userId); if ($profile->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } - $tokens = $profile->getAttribute('tokens', []); - $verifiedToken = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_RECOVERY, $secret); + $verifiedToken = $profile->tokenVerify(TOKEN_TYPE_RECOVERY, $secret, $proofForToken); if (!$verifiedToken) { throw new Exception(Exception::USER_INVALID_TOKEN); @@ -3596,12 +3676,14 @@ App::put('/v1/account/recovery') Authorization::setRole(Role::user($profile->getId())->toString()); - $newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); + $newPassword = $proofForPassword->hash($password); + $hash = ProofsPassword::createHash($profile->getAttribute('hash'), $profile->getAttribute('hashOptions')); $historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0; $history = $profile->getAttribute('passwordHistory', []); + if ($historyLimit > 0) { - $validator = new PasswordHistory($history, $profile->getAttribute('hash'), $profile->getAttribute('hashOptions')); + $validator = new PasswordHistory($history, $hash); if (!$validator->isValid($password)) { throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED); } @@ -3613,12 +3695,12 @@ App::put('/v1/account/recovery') $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]); $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile - ->setAttribute('password', $newPassword) - ->setAttribute('passwordHistory', $history) - ->setAttribute('passwordUpdate', DateTime::now()) - ->setAttribute('hash', Auth::DEFAULT_ALGO) - ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS) - ->setAttribute('emailVerification', true)); + ->setAttribute('password', $newPassword) + ->setAttribute('passwordHistory', $history) + ->setAttribute('passwordUpdate', DateTime::now()) + ->setAttribute('hash', $proofForPassword->getHash()->getName()) + ->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions()) + ->setAttribute('emailVerification', true)); $user->setAttributes($profile->getArrayCopy()); @@ -3692,7 +3774,8 @@ App::post('/v1/account/verifications/email') ->inject('locale') ->inject('queueForEvents') ->inject('queueForMails') - ->action(function (string $url, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) { + ->inject('proofForToken') + ->action(function (string $url, Request $request, Response $response, Document $project, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken) { if (empty(System::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); @@ -3707,15 +3790,15 @@ App::post('/v1/account/verifications/email') throw new Exception(Exception::USER_EMAIL_ALREADY_VERIFIED); } - $verificationSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_VERIFICATION); - $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM)); + $verificationSecret = $proofForToken->generate(); + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_CONFIRM)); $verification = new Document([ '$id' => ID::unique(), 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), - 'type' => Auth::TOKEN_TYPE_VERIFICATION, - 'secret' => Auth::hash($verificationSecret), // One way hash encryption to protect DB leak + 'type' => TOKEN_TYPE_VERIFICATION, + 'secret' => $proofForToken->hash($verificationSecret), // One way hash encryption to protect DB leak 'expire' => $expire, 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), @@ -3904,16 +3987,16 @@ App::put('/v1/account/verifications/email') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { - + ->inject('proofForToken') + ->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsToken $proofForToken) { + /** @var Appwrite\Utopia\Database\Documents\User $profile */ $profile = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId)); if ($profile->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } - $tokens = $profile->getAttribute('tokens', []); - $verifiedToken = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_VERIFICATION, $secret); + $verifiedToken = $profile->tokenVerify(TOKEN_TYPE_VERIFICATION, $secret, $proofForToken); if (!$verifiedToken) { throw new Exception(Exception::USER_INVALID_TOKEN); @@ -3978,7 +4061,8 @@ App::post('/v1/account/verifications/phone') ->inject('timelimit') ->inject('queueForStatsUsage') ->inject('plan') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan) { + ->inject('proofForCode') + ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, ProofsCode $proofForCode) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -4003,15 +4087,15 @@ App::post('/v1/account/verifications/phone') } } - $secret ??= Auth::codeGenerator(); - $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM)); + $secret ??= $proofForCode->generate(); + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_CONFIRM)); $verification = new Document([ '$id' => ID::unique(), 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), - 'type' => Auth::TOKEN_TYPE_PHONE, - 'secret' => Auth::hash($secret), + 'type' => TOKEN_TYPE_PHONE, + 'secret' => $proofForCode->hash($secret), 'expire' => $expire, 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), @@ -4122,15 +4206,16 @@ App::put('/v1/account/verifications/phone') ->inject('user') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { - + ->inject('proofForCode') + ->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsCode $proofForCode) { + /** @var Appwrite\Utopia\Database\Documents\User $profile */ $profile = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId)); if ($profile->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } - $verifiedToken = Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_PHONE, $secret); + $verifiedToken = $profile->tokenVerify(TOKEN_TYPE_PHONE, $secret, $proofForCode); if (!$verifiedToken) { throw new Exception(Exception::USER_INVALID_TOKEN); @@ -4158,943 +4243,6 @@ App::put('/v1/account/verifications/phone') $response->dynamic($verificationDocument, Response::MODEL_TOKEN); }); -App::patch('/v1/account/mfa') - ->desc('Update MFA') - ->groups(['api', 'account']) - ->label('event', 'users.[userId].update.mfa') - ->label('scope', 'account') - ->label('audits.event', 'user.update') - ->label('audits.resource', 'user/{response.$id}') - ->label('audits.userId', '{response.$id}') - ->label('sdk', new Method( - namespace: 'account', - group: 'mfa', - name: 'updateMFA', - description: '/docs/references/account/update-mfa.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_USER, - ) - ], - contentType: ContentType::JSON - )) - ->param('mfa', null, new Boolean(), 'Enable or disable MFA.') - ->inject('requestTimestamp') - ->inject('response') - ->inject('user') - ->inject('session') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (bool $mfa, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $session, Database $dbForProject, Event $queueForEvents) { - - $user->setAttribute('mfa', $mfa); - - $user = $dbForProject->updateDocument('users', $user->getId(), $user); - - if ($mfa) { - $factors = $session->getAttribute('factors', []); - $totp = TOTP::getAuthenticatorFromUser($user); - if ($totp !== null && $totp->getAttribute('verified', false)) { - $factors[] = Type::TOTP; - } - if ($user->getAttribute('email', false) && $user->getAttribute('emailVerification', false)) { - $factors[] = Type::EMAIL; - } - if ($user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false)) { - $factors[] = Type::PHONE; - } - $factors = \array_values(\array_unique($factors)); - - $session->setAttribute('factors', $factors); - $dbForProject->updateDocument('sessions', $session->getId(), $session); - } - - $queueForEvents->setParam('userId', $user->getId()); - - $response->dynamic($user, Response::MODEL_ACCOUNT); - }); - -App::get('/v1/account/mfa/factors') - ->desc('List factors') - ->groups(['api', 'account', 'mfa']) - ->label('scope', 'account') - ->label('sdk', [ - new Method( - namespace: 'account', - group: 'mfa', - name: 'listMfaFactors', - description: '/docs/references/account/list-mfa-factors.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_MFA_FACTORS, - ) - ], - contentType: ContentType::JSON, - deprecated: new Deprecated( - since: '1.8.0', - replaceWith: 'account.listMFAFactors', - ), - ), - new Method( - namespace: 'account', - group: 'mfa', - name: 'listMFAFactors', - description: '/docs/references/account/list-mfa-factors.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_MFA_FACTORS, - ) - ], - contentType: ContentType::JSON - ) - ]) - ->inject('response') - ->inject('user') - ->action(function (Response $response, Document $user) { - - $mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []); - $recoveryCodeEnabled = \is_array($mfaRecoveryCodes) && \count($mfaRecoveryCodes) > 0; - - $totp = TOTP::getAuthenticatorFromUser($user); - - $factors = new Document([ - Type::TOTP => $totp !== null && $totp->getAttribute('verified', false), - Type::EMAIL => $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false), - Type::PHONE => $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false), - Type::RECOVERY_CODE => $recoveryCodeEnabled - ]); - - $response->dynamic($factors, Response::MODEL_MFA_FACTORS); - }); - -App::post('/v1/account/mfa/authenticators/:type') - ->desc('Create authenticator') - ->groups(['api', 'account']) - ->label('event', 'users.[userId].update.mfa') - ->label('scope', 'account') - ->label('audits.event', 'user.update') - ->label('audits.resource', 'user/{response.$id}') - ->label('audits.userId', '{response.$id}') - ->label('sdk', [ - new Method( - namespace: 'account', - group: 'mfa', - name: 'createMfaAuthenticator', - description: '/docs/references/account/create-mfa-authenticator.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_MFA_TYPE, - ) - ], - contentType: ContentType::JSON, - deprecated: new Deprecated( - since: '1.8.0', - replaceWith: 'account.createMFAAuthenticator', - ), - ), - new Method( - namespace: 'account', - group: 'mfa', - name: 'createMFAAuthenticator', - description: '/docs/references/account/create-mfa-authenticator.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_MFA_TYPE, - ) - ], - contentType: ContentType::JSON - ) - ]) - ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator. Must be `' . Type::TOTP . '`') - ->inject('requestTimestamp') - ->inject('response') - ->inject('project') - ->inject('user') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $type, ?\DateTime $requestTimestamp, Response $response, Document $project, Document $user, Database $dbForProject, Event $queueForEvents) { - - $otp = (match ($type) { - Type::TOTP => new TOTP(), - default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Unknown type.') // Ideally never happens if param validator stays always in sync - }); - - $otp->setLabel($user->getAttribute('email')); - $otp->setIssuer($project->getAttribute('name')); - - $authenticator = TOTP::getAuthenticatorFromUser($user); - - if ($authenticator) { - if ($authenticator->getAttribute('verified')) { - throw new Exception(Exception::USER_AUTHENTICATOR_ALREADY_VERIFIED); - } - $dbForProject->deleteDocument('authenticators', $authenticator->getId()); - } - - $authenticator = new Document([ - '$id' => ID::unique(), - 'userId' => $user->getId(), - 'userInternalId' => $user->getSequence(), - 'type' => Type::TOTP, - 'verified' => false, - 'data' => [ - 'secret' => $otp->getSecret(), - ], - '$permissions' => [ - Permission::read(Role::user($user->getId())), - Permission::update(Role::user($user->getId())), - Permission::delete(Role::user($user->getId())), - ] - ]); - - $model = new Document([ - 'secret' => $otp->getSecret(), - 'uri' => $otp->getProvisioningUri() - ]); - - $authenticator = $dbForProject->createDocument('authenticators', $authenticator); - $dbForProject->purgeCachedDocument('users', $user->getId()); - - $queueForEvents->setParam('userId', $user->getId()); - - $response->dynamic($model, Response::MODEL_MFA_TYPE); - }); - -App::put('/v1/account/mfa/authenticators/:type') - ->desc('Update authenticator (confirmation)') - ->groups(['api', 'account']) - ->label('event', 'users.[userId].update.mfa') - ->label('scope', 'account') - ->label('audits.event', 'user.update') - ->label('audits.resource', 'user/{response.$id}') - ->label('audits.userId', '{response.$id}') - ->label('sdk', [ - new Method( - namespace: 'account', - group: 'mfa', - name: 'updateMfaAuthenticator', - description: '/docs/references/account/update-mfa-authenticator.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_USER, - ) - ], - contentType: ContentType::JSON, - deprecated: new Deprecated( - since: '1.8.0', - replaceWith: 'account.updateMFAAuthenticator', - ), - ), - new Method( - namespace: 'account', - group: 'mfa', - name: 'updateMFAAuthenticator', - description: '/docs/references/account/update-mfa-authenticator.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_USER, - ) - ], - contentType: ContentType::JSON - ) - ]) - ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.') - ->param('otp', '', new Text(256), 'Valid verification token.') - ->inject('response') - ->inject('user') - ->inject('session') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $type, string $otp, Response $response, Document $user, Document $session, Database $dbForProject, Event $queueForEvents) { - - $authenticator = (match ($type) { - Type::TOTP => TOTP::getAuthenticatorFromUser($user), - default => null - }); - - if ($authenticator === null) { - throw new Exception(Exception::USER_AUTHENTICATOR_NOT_FOUND); - } - - if ($authenticator->getAttribute('verified')) { - throw new Exception(Exception::USER_AUTHENTICATOR_ALREADY_VERIFIED); - } - - $success = (match ($type) { - Type::TOTP => Challenge\TOTP::verify($user, $otp), - default => false - }); - - if (!$success) { - throw new Exception(Exception::USER_INVALID_TOKEN); - } - - $authenticator->setAttribute('verified', true); - - $dbForProject->updateDocument('authenticators', $authenticator->getId(), $authenticator); - $dbForProject->purgeCachedDocument('users', $user->getId()); - - $factors = $session->getAttribute('factors', []); - $factors[] = $type; - $factors = \array_values(\array_unique($factors)); - - $session->setAttribute('factors', $factors); - $dbForProject->updateDocument('sessions', $session->getId(), $session); - - $queueForEvents->setParam('userId', $user->getId()); - - $response->dynamic($user, Response::MODEL_ACCOUNT); - }); - -App::post('/v1/account/mfa/recovery-codes') - ->desc('Create MFA recovery codes') - ->groups(['api', 'account']) - ->label('event', 'users.[userId].update.mfa') - ->label('scope', 'account') - ->label('audits.event', 'user.update') - ->label('audits.resource', 'user/{response.$id}') - ->label('audits.userId', '{response.$id}') - ->label('sdk', [ - new Method( - namespace: 'account', - group: 'mfa', - name: 'createMfaRecoveryCodes', - description: '/docs/references/account/create-mfa-recovery-codes.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_MFA_RECOVERY_CODES, - ) - ], - contentType: ContentType::JSON, - deprecated: new Deprecated( - since: '1.8.0', - replaceWith: 'account.createMFARecoveryCodes', - ), - ), - new Method( - namespace: 'account', - group: 'mfa', - name: 'createMFARecoveryCodes', - description: '/docs/references/account/create-mfa-recovery-codes.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_MFA_RECOVERY_CODES, - ) - ], - contentType: ContentType::JSON - ) - ]) - ->inject('response') - ->inject('user') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { - - $mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []); - - if (!empty($mfaRecoveryCodes)) { - throw new Exception(Exception::USER_RECOVERY_CODES_ALREADY_EXISTS); - } - - $mfaRecoveryCodes = Type::generateBackupCodes(); - $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); - $dbForProject->updateDocument('users', $user->getId(), $user); - - $queueForEvents->setParam('userId', $user->getId()); - - $document = new Document([ - 'recoveryCodes' => $mfaRecoveryCodes - ]); - - $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); - }); - -App::patch('/v1/account/mfa/recovery-codes') - ->desc('Update MFA recovery codes (regenerate)') - ->groups(['api', 'account', 'mfaProtected']) - ->label('event', 'users.[userId].update.mfa') - ->label('scope', 'account') - ->label('audits.event', 'user.update') - ->label('audits.resource', 'user/{response.$id}') - ->label('audits.userId', '{response.$id}') - ->label('sdk', [ - new Method( - namespace: 'account', - group: 'mfa', - name: 'updateMfaRecoveryCodes', - description: '/docs/references/account/update-mfa-recovery-codes.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_MFA_RECOVERY_CODES, - ) - ], - contentType: ContentType::JSON, - deprecated: new Deprecated( - since: '1.8.0', - replaceWith: 'account.updateMFARecoveryCodes', - ), - ), - new Method( - namespace: 'account', - group: 'mfa', - name: 'updateMFARecoveryCodes', - description: '/docs/references/account/update-mfa-recovery-codes.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_MFA_RECOVERY_CODES, - ) - ], - contentType: ContentType::JSON - ) - ]) - ->inject('dbForProject') - ->inject('response') - ->inject('user') - ->inject('queueForEvents') - ->action(function (Database $dbForProject, Response $response, Document $user, Event $queueForEvents) { - - $mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []); - if (empty($mfaRecoveryCodes)) { - throw new Exception(Exception::USER_RECOVERY_CODES_NOT_FOUND); - } - - $mfaRecoveryCodes = Type::generateBackupCodes(); - $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); - $dbForProject->updateDocument('users', $user->getId(), $user); - - $queueForEvents->setParam('userId', $user->getId()); - - $document = new Document([ - 'recoveryCodes' => $mfaRecoveryCodes - ]); - - $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); - }); - -App::get('/v1/account/mfa/recovery-codes') - ->desc('List MFA recovery codes') - ->groups(['api', 'account', 'mfaProtected']) - ->label('scope', 'account') - ->label('sdk', [ - new Method( - namespace: 'account', - group: 'mfa', - name: 'getMfaRecoveryCodes', - description: '/docs/references/account/get-mfa-recovery-codes.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_MFA_RECOVERY_CODES, - ) - ], - contentType: ContentType::JSON, - deprecated: new Deprecated( - since: '1.8.0', - replaceWith: 'account.getMFARecoveryCodes', - ), - ), - new Method( - namespace: 'account', - group: 'mfa', - name: 'getMFARecoveryCodes', - description: '/docs/references/account/get-mfa-recovery-codes.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_MFA_RECOVERY_CODES, - ) - ], - contentType: ContentType::JSON - ) - ]) - ->inject('response') - ->inject('user') - ->action(function (Response $response, Document $user) { - - $mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []); - - if (empty($mfaRecoveryCodes)) { - throw new Exception(Exception::USER_RECOVERY_CODES_NOT_FOUND); - } - - $document = new Document([ - 'recoveryCodes' => $mfaRecoveryCodes - ]); - - $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); - }); - -App::delete('/v1/account/mfa/authenticators/:type') - ->desc('Delete authenticator') - ->groups(['api', 'account', 'mfaProtected']) - ->label('event', 'users.[userId].delete.mfa') - ->label('scope', 'account') - ->label('audits.event', 'user.update') - ->label('audits.resource', 'user/{response.$id}') - ->label('audits.userId', '{response.$id}') - ->label('sdk', [ - new Method( - namespace: 'account', - group: 'mfa', - name: 'deleteMfaAuthenticator', - description: '/docs/references/account/delete-mfa-authenticator.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE, - deprecated: new Deprecated( - since: '1.8.0', - replaceWith: 'account.deleteMFAAuthenticator', - ), - ), - new Method( - namespace: 'account', - group: 'mfa', - name: 'deleteMFAAuthenticator', - description: '/docs/references/account/delete-mfa-authenticator.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE - ) - ]) - ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.') - ->inject('response') - ->inject('user') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $type, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { - - $authenticator = (match ($type) { - Type::TOTP => TOTP::getAuthenticatorFromUser($user), - default => null - }); - - if (!$authenticator) { - throw new Exception(Exception::USER_AUTHENTICATOR_NOT_FOUND); - } - - $dbForProject->deleteDocument('authenticators', $authenticator->getId()); - $dbForProject->purgeCachedDocument('users', $user->getId()); - - $queueForEvents->setParam('userId', $user->getId()); - - $response->noContent(); - }); - -App::post('/v1/account/mfa/challenge') - ->desc('Create MFA challenge') - ->groups(['api', 'account', 'mfa']) - ->label('scope', 'account') - ->label('event', 'users.[userId].challenges.[challengeId].create') - ->label('audits.event', 'challenge.create') - ->label('audits.resource', 'user/{response.userId}') - ->label('audits.userId', '{response.userId}') - ->label('sdk', [ - new Method( - namespace: 'account', - group: 'mfa', - name: 'createMfaChallenge', - description: '/docs/references/account/create-mfa-challenge.md', - auth: [], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_MFA_CHALLENGE, - ) - ], - contentType: ContentType::JSON, - deprecated: new Deprecated( - since: '1.8.0', - replaceWith: 'account.createMFAChallenge', - ), - ), - new Method( - namespace: 'account', - group: 'mfa', - name: 'createMFAChallenge', - description: '/docs/references/account/create-mfa-challenge.md', - auth: [], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_MFA_CHALLENGE, - ) - ], - contentType: ContentType::JSON - ) - ]) - ->label('abuse-limit', 10) - ->label('abuse-key', 'url:{url},userId:{userId}') - ->param('factor', '', new WhiteList([Type::EMAIL, Type::PHONE, Type::TOTP, Type::RECOVERY_CODE]), 'Factor used for verification. Must be one of following: `' . Type::EMAIL . '`, `' . Type::PHONE . '`, `' . Type::TOTP . '`, `' . Type::RECOVERY_CODE . '`.') - ->inject('response') - ->inject('dbForProject') - ->inject('user') - ->inject('locale') - ->inject('project') - ->inject('request') - ->inject('queueForEvents') - ->inject('queueForMessaging') - ->inject('queueForMails') - ->inject('timelimit') - ->inject('queueForStatsUsage') - ->inject('plan') - ->action(function (string $factor, Response $response, Database $dbForProject, Document $user, Locale $locale, Document $project, Request $request, Event $queueForEvents, Messaging $queueForMessaging, Mail $queueForMails, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan) { - - $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM)); - $code = Auth::codeGenerator(); - $challenge = new Document([ - 'userId' => $user->getId(), - 'userInternalId' => $user->getSequence(), - 'type' => $factor, - 'token' => Auth::tokenGenerator(), - 'code' => $code, - 'expire' => $expire, - '$permissions' => [ - Permission::read(Role::user($user->getId())), - Permission::update(Role::user($user->getId())), - Permission::delete(Role::user($user->getId())), - ], - ]); - - $challenge = $dbForProject->createDocument('challenges', $challenge); - - switch ($factor) { - case Type::PHONE: - if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { - throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); - } - if (empty($user->getAttribute('phone'))) { - throw new Exception(Exception::USER_PHONE_NOT_FOUND); - } - if (!$user->getAttribute('phoneVerification')) { - throw new Exception(Exception::USER_PHONE_NOT_VERIFIED); - } - - $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl'); - - $customTemplate = $project->getAttribute('templates', [])['sms.mfaChallenge-' . $locale->default] ?? []; - if (!empty($customTemplate)) { - $message = $customTemplate['message'] ?? $message; - } - - $messageContent = Template::fromString($locale->getText("sms.verification.body")); - $messageContent - ->setParam('{{project}}', $project->getAttribute('name')) - ->setParam('{{secret}}', $code); - $messageContent = \strip_tags($messageContent->render()); - $message = $message->setParam('{{token}}', $messageContent); - - $message = $message->render(); - - $phone = $user->getAttribute('phone'); - $queueForMessaging - ->setType(MESSAGE_SEND_TYPE_INTERNAL) - ->setMessage(new Document([ - '$id' => $challenge->getId(), - 'data' => [ - 'content' => $code, - ], - ])) - ->setRecipients([$phone]) - ->setProviderType(MESSAGE_TYPE_SMS); - - if (isset($plan['authPhone'])) { - $timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days - $timelimit - ->setParam('{organizationId}', $project->getAttribute('teamId')); - - $abuse = new Abuse($timelimit); - if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') { - $helper = PhoneNumberUtil::getInstance(); - $countryCode = $helper->parse($phone)->getCountryCode(); - - if (!empty($countryCode)) { - $queueForStatsUsage - ->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1); - } - } - $queueForStatsUsage - ->addMetric(METRIC_AUTH_METHOD_PHONE, 1) - ->setProject($project) - ->trigger(); - } - break; - case Type::EMAIL: - if (empty(System::getEnv('_APP_SMTP_HOST'))) { - throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); - } - if (empty($user->getAttribute('email'))) { - throw new Exception(Exception::USER_EMAIL_NOT_FOUND); - } - if (!$user->getAttribute('emailVerification')) { - throw new Exception(Exception::USER_EMAIL_NOT_VERIFIED); - } - - $subject = $locale->getText("emails.mfaChallenge.subject"); - $preview = $locale->getText("emails.mfaChallenge.preview"); - $heading = $locale->getText("emails.mfaChallenge.heading"); - - $customTemplate = $project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ?? []; - $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); - - $validator = new FileName(); - if (!$validator->isValid($smtpBaseTemplate)) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path'); - } - - $bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl'; - - $detector = new Detector($request->getUserAgent('UNKNOWN')); - $agentOs = $detector->getOS(); - $agentClient = $detector->getClient(); - $agentDevice = $detector->getDevice(); - - $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-mfa-challenge.tpl'); - $message - ->setParam('{{hello}}', $locale->getText("emails.mfaChallenge.hello")) - ->setParam('{{description}}', $locale->getText("emails.mfaChallenge.description")) - ->setParam('{{clientInfo}}', $locale->getText("emails.mfaChallenge.clientInfo")) - ->setParam('{{thanks}}', $locale->getText("emails.mfaChallenge.thanks")) - ->setParam('{{signature}}', $locale->getText("emails.mfaChallenge.signature")); - - $body = $message->render(); - - $smtp = $project->getAttribute('smtp', []); - $smtpEnabled = $smtp['enabled'] ?? false; - - $senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); - $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); - $replyTo = ""; - - if ($smtpEnabled) { - if (!empty($smtp['senderEmail'])) { - $senderEmail = $smtp['senderEmail']; - } - if (!empty($smtp['senderName'])) { - $senderName = $smtp['senderName']; - } - if (!empty($smtp['replyTo'])) { - $replyTo = $smtp['replyTo']; - } - - $queueForMails - ->setSmtpHost($smtp['host'] ?? '') - ->setSmtpPort($smtp['port'] ?? '') - ->setSmtpUsername($smtp['username'] ?? '') - ->setSmtpPassword($smtp['password'] ?? '') - ->setSmtpSecure($smtp['secure'] ?? ''); - - if (!empty($customTemplate)) { - if (!empty($customTemplate['senderEmail'])) { - $senderEmail = $customTemplate['senderEmail']; - } - if (!empty($customTemplate['senderName'])) { - $senderName = $customTemplate['senderName']; - } - if (!empty($customTemplate['replyTo'])) { - $replyTo = $customTemplate['replyTo']; - } - - $body = $customTemplate['message'] ?? ''; - $subject = $customTemplate['subject'] ?? $subject; - } - - $queueForMails - ->setSmtpReplyTo($replyTo) - ->setSmtpSenderEmail($senderEmail) - ->setSmtpSenderName($senderName); - } - - $emailVariables = [ - 'heading' => $heading, - 'direction' => $locale->getText('settings.direction'), - // {{user}}, {{project}} and {{otp}} are required in the templates - 'user' => $user->getAttribute('name'), - 'project' => $project->getAttribute('name'), - 'otp' => $code, - 'agentDevice' => $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN', - 'agentClient' => $agentClient['clientName'] ?? 'UNKNOWN', - 'agentOs' => $agentOs['osName'] ?? 'UNKNOWN', - ]; - - if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) { - $emailVariables = array_merge($emailVariables, [ - 'accentColor' => APP_EMAIL_ACCENT_COLOR, - 'logoUrl' => APP_EMAIL_LOGO_URL, - 'twitterUrl' => APP_SOCIAL_TWITTER, - 'discordUrl' => APP_SOCIAL_DISCORD, - 'githubUrl' => APP_SOCIAL_GITHUB_APPWRITE, - 'termsUrl' => APP_EMAIL_TERMS_URL, - 'privacyUrl' => APP_EMAIL_PRIVACY_URL, - ]); - } - - $queueForMails - ->setSubject($subject) - ->setPreview($preview) - ->setBody($body) - ->setBodyTemplate($bodyTemplate) - ->setVariables($emailVariables) - ->setRecipient($user->getAttribute('email')) - ->trigger(); - break; - } - - $queueForEvents - ->setParam('userId', $user->getId()) - ->setParam('challengeId', $challenge->getId()); - - $response->dynamic($challenge, Response::MODEL_MFA_CHALLENGE); - }); - -App::put('/v1/account/mfa/challenge') - ->desc('Update MFA challenge (confirmation)') - ->groups(['api', 'account', 'mfa']) - ->label('scope', 'account') - ->label('event', 'users.[userId].sessions.[sessionId].create') - ->label('audits.event', 'challenges.update') - ->label('audits.resource', 'user/{response.userId}') - ->label('audits.userId', '{response.userId}') - ->label('sdk', [ - new Method( - namespace: 'account', - group: 'mfa', - name: 'updateMfaChallenge', - description: '/docs/references/account/update-mfa-challenge.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_SESSION, - ) - ], - contentType: ContentType::JSON, - deprecated: new Deprecated( - since: '1.8.0', - replaceWith: 'account.updateMFAChallenge', - ), - ), - new Method( - namespace: 'account', - group: 'mfa', - name: 'updateMFAChallenge', - description: '/docs/references/account/update-mfa-challenge.md', - auth: [AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_SESSION, - ) - ], - contentType: ContentType::JSON - ) - ]) - ->label('abuse-limit', 10) - ->label('abuse-key', 'url:{url},challengeId:{param-challengeId}') - ->param('challengeId', '', new Text(256), 'ID of the challenge.') - ->param('otp', '', new Text(256), 'Valid verification token.') - ->inject('project') - ->inject('response') - ->inject('user') - ->inject('session') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $challengeId, string $otp, Document $project, Response $response, Document $user, Document $session, Database $dbForProject, Event $queueForEvents) { - - $challenge = $dbForProject->getDocument('challenges', $challengeId); - - if ($challenge->isEmpty()) { - throw new Exception(Exception::USER_INVALID_TOKEN); - } - - $type = $challenge->getAttribute('type'); - - $recoveryCodeChallenge = function (Document $challenge, Document $user, string $otp) use ($dbForProject) { - if ( - $challenge->isSet('type') && - $challenge->getAttribute('type') === \strtolower(Type::RECOVERY_CODE) - ) { - $mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []); - if (in_array($otp, $mfaRecoveryCodes)) { - $mfaRecoveryCodes = array_diff($mfaRecoveryCodes, [$otp]); - $mfaRecoveryCodes = array_values($mfaRecoveryCodes); - $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); - $dbForProject->updateDocument('users', $user->getId(), $user); - - return true; - } - - return false; - } - - return false; - }; - - $success = (match ($type) { - Type::TOTP => Challenge\TOTP::challenge($challenge, $user, $otp), - Type::PHONE => Challenge\Phone::challenge($challenge, $user, $otp), - Type::EMAIL => Challenge\Email::challenge($challenge, $user, $otp), - \strtolower(Type::RECOVERY_CODE) => $recoveryCodeChallenge($challenge, $user, $otp), - default => false - }); - - if (!$success) { - throw new Exception(Exception::USER_INVALID_TOKEN); - } - - $dbForProject->deleteDocument('challenges', $challengeId); - $dbForProject->purgeCachedDocument('users', $user->getId()); - - $factors = $session->getAttribute('factors', []); - $factors[] = $type; - $factors = \array_values(\array_unique($factors)); - - $session - ->setAttribute('factors', $factors) - ->setAttribute('mfaUpdatedAt', DateTime::now()); - - $dbForProject->updateDocument('sessions', $session->getId(), $session); - - $queueForEvents - ->setParam('userId', $user->getId()) - ->setParam('sessionId', $session->getId()); - - $response->dynamic($session, Response::MODEL_SESSION); - }); - App::post('/v1/account/targets/push') ->desc('Create push target') ->groups(['api', 'account']) @@ -5124,7 +4272,9 @@ App::post('/v1/account/targets/push') ->inject('request') ->inject('response') ->inject('dbForProject') - ->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { + ->inject('store') + ->inject('proofForToken') + ->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, User $user, Request $request, Response $response, Database $dbForProject, Store $store, ProofsToken $proofForToken) { $targetId = $targetId == 'unique()' ? ID::unique() : $targetId; $provider = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId)); @@ -5140,7 +4290,7 @@ App::post('/v1/account/targets/push') $device = $detector->getDevice(); - $sessionId = Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret); + $sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken); $session = $dbForProject->getDocument('sessions', $sessionId); try { @@ -5318,7 +4468,7 @@ App::get('/v1/account/identities') ->inject('response') ->inject('user') ->inject('dbForProject') - ->action(function (array $queries, bool $includeTotal, Response $response, Document $user, Database $dbForProject) { + ->action(function (array $queries, bool $includeTotal, Response $response, User $user, Database $dbForProject) { try { $queries = Query::parseQueries($queries); diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index 482b38d698..6ad5087765 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -1,6 +1,5 @@ getAttribute('apis', [])) && !$project->getAttribute('apis', [])['graphql'] - && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + && !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index a3b40ce8bd..02fee04eac 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1,7 +1,6 @@ APP_LIMIT_USER_SESSIONS_DEFAULT, 'passwordHistory' => 0, 'passwordDictionary' => false, - 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, + 'duration' => TOKEN_EXPIRATION_LOGIN_LONG, 'personalDataCheck' => false, 'mockNumbers' => [], 'sessionAlerts' => false, diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index b973b18d44..185c737399 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -2,7 +2,6 @@ use Ahc\Jwt\JWT; use Ahc\Jwt\JWTException; -use Appwrite\Auth\Auth; use Appwrite\ClamAV\Network; use Appwrite\Event\Delete; use Appwrite\Event\Event; @@ -13,6 +12,7 @@ use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\MethodType; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries\Buckets; use Appwrite\Utopia\Database\Validator\Queries\Files; @@ -437,8 +437,8 @@ App::post('/v1/storage/buckets/:bucketId/files') $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); @@ -470,7 +470,7 @@ App::post('/v1/storage/buckets/:bucketId/files') // Users can only manage their own roles, API keys and Admin users can manage any $roles = Authorization::getRoles(); - if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { + if (!User::isApp($roles) && !User::isPrivileged($roles)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { $permission = Permission::parse($permission); @@ -799,8 +799,8 @@ App::get('/v1/storage/buckets/:bucketId/files') ->action(function (string $bucketId, array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject, string $mode) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); @@ -900,8 +900,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId') ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); @@ -982,8 +982,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') /* @type Document $bucket */ $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); @@ -1127,7 +1127,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg']; //Do not update transformedAt if it's a console user - if (!Auth::isPrivilegedUser(Authorization::getRoles())) { + if (!User::isPrivileged(Authorization::getRoles())) { $transformedAt = $file->getAttribute('transformedAt', ''); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) { $file->setAttribute('transformedAt', DateTime::now()); @@ -1178,8 +1178,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') /* @type Document $bucket */ $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); @@ -1339,8 +1339,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') /* @type Document $bucket */ $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); @@ -1509,10 +1509,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') } $isInternal = $decoded['internal'] ?? false; + $disposition = $decoded['disposition'] ?? 'inline'; $dbForProject = $isInternal ? $dbForPlatform : $dbForProject; - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { @@ -1565,7 +1566,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') ->setContentType($contentType) ->addHeader('Content-Security-Policy', 'script-src none;') ->addHeader('X-Content-Type-Options', 'nosniff') - ->addHeader('Content-Disposition', 'inline; filename="' . $file->getAttribute('name', '') . '"') + ->addHeader('Content-Disposition', $disposition . '; filename="' . $file->getAttribute('name', '') . '"') ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days ->addHeader('X-Peak', \memory_get_peak_usage()); @@ -1668,8 +1669,8 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); @@ -1698,7 +1699,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') // Users can only manage their own roles, API keys and Admin users can manage any $roles = Authorization::getRoles(); - if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\is_null($permissions)) { + if (!User::isApp($roles) && !User::isPrivileged($roles) && !\is_null($permissions)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { $permission = Permission::parse($permission); @@ -1782,8 +1783,8 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 5c22bf1e18..7dccbe84b3 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1,6 +1,5 @@ inject('queueForEvents') ->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - $isAppUser = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAppUser = User::isApp(Authorization::getRoles()); $teamId = $teamId == 'unique()' ? ID::unique() : $teamId; @@ -176,6 +179,7 @@ App::get('/v1/teams') ->inject('dbForProject') ->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) { + try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -474,10 +478,9 @@ App::post('/v1/teams/:teamId/memberships') ->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) ->param('roles', [], function (Document $project, Database $dbForProject) { if ($project->getId() === 'console') { - ; $roles = array_keys(Config::getParam('roles', [])); - array_filter($roles, function ($role) { - return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]); + $roles = array_filter($roles, function ($role) { + return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]); }); return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE); } @@ -496,9 +499,11 @@ App::post('/v1/teams/:teamId/memberships') ->inject('timelimit') ->inject('queueForStatsUsage') ->inject('plan') - ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan) { - $isAppUser = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + ->inject('proofForPassword') + ->inject('proofForToken') + ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) { + $isAppUser = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $url = htmlentities($url); if (empty($url)) { @@ -568,6 +573,8 @@ App::post('/v1/teams/:teamId/memberships') } try { + $userId = ID::unique(); + $hash = $proofForPassword->hash($proofForPassword->generate()); $emailCanonical = new Email($email); } catch (Throwable) { $emailCanonical = null; @@ -588,9 +595,9 @@ App::post('/v1/teams/:teamId/memberships') 'emailVerification' => false, 'status' => true, // TODO: Set password empty? - 'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS), - 'hash' => Auth::DEFAULT_ALGO, - 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, + 'password' => $hash, + 'hash' => $proofForPassword->getHash()->getName(), + 'hashOptions' => $proofForPassword->getHash()->getOptions(), /** * Set the password update time to 0 for users created using * team invite and OAuth to allow password updates without an @@ -630,7 +637,7 @@ App::post('/v1/teams/:teamId/memberships') Query::equal('teamInternalId', [$team->getSequence()]), ]); - $secret = Auth::tokenGenerator(); + $secret = $proofForToken->generate(); if ($membership->isEmpty()) { $membershipId = ID::unique(); $membership = new Document([ @@ -650,7 +657,7 @@ App::post('/v1/teams/:teamId/memberships') 'invited' => DateTime::now(), 'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null, 'confirm' => ($isPrivilegedUser || $isAppUser), - 'secret' => Auth::hash($secret), + 'secret' => $proofForToken->hash($secret), 'search' => implode(' ', [$membershipId, $invitee->getId()]) ]); @@ -661,9 +668,8 @@ App::post('/v1/teams/:teamId/memberships') if ($isPrivilegedUser || $isAppUser) { Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); } - } elseif ($membership->getAttribute('confirm') === false) { - $membership->setAttribute('secret', Auth::hash($secret)); + $membership->setAttribute('secret', $proofForToken->hash($secret)); $membership->setAttribute('invited', DateTime::now()); if ($isPrivilegedUser || $isAppUser) { @@ -766,7 +772,6 @@ App::post('/v1/teams/:teamId/memberships') ->setName($invitee->getAttribute('name', '')) ->setVariables($emailVariables) ->trigger(); - } elseif (!empty($phone)) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); @@ -930,8 +935,8 @@ App::get('/v1/teams/:teamId/memberships') ]; $roles = Authorization::getRoles(); - $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); + $isPrivilegedUser = User::isPrivileged($roles); + $isAppUser = User::isApp($roles); $membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) { return $privacy || $isPrivilegedUser || $isAppUser; @@ -1021,8 +1026,8 @@ App::get('/v1/teams/:teamId/memberships/:membershipId') ]; $roles = Authorization::getRoles(); - $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); + $isPrivilegedUser = User::isPrivileged($roles); + $isAppUser = User::isApp($roles); $membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) { return $privacy || $isPrivilegedUser || $isAppUser; @@ -1087,8 +1092,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ->param('roles', [], function (Document $project, Database $dbForProject) { if ($project->getId() === 'console') { $roles = array_keys(Config::getParam('roles', [])); - array_filter($roles, function ($role) { - return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]); + $roles = array_filter($roles, function ($role) { + return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]); }); return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE); } @@ -1117,8 +1122,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') throw new Exception(Exception::USER_NOT_FOUND); } - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - $isAppUser = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAppUser = User::isApp(Authorization::getRoles()); $isOwner = Authorization::isRole('team:' . $team->getId() . '/owner'); if ($project->getId() === 'console') { @@ -1203,7 +1208,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->inject('project') ->inject('geodb') ->inject('queueForEvents') - ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents) { + ->inject('store') + ->inject('proofForToken') + ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) { $protocol = $request->getProtocol(); $membership = $dbForProject->getDocument('memberships', $membershipId); @@ -1222,7 +1229,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') throw new Exception(Exception::TEAM_MEMBERSHIP_MISMATCH); } - if (Auth::hash($secret) !== $membership->getAttribute('secret')) { + if (!$proofForToken->verify($secret, $membership->getAttribute('secret'))) { throw new Exception(Exception::TEAM_INVALID_SECRET); } @@ -1256,9 +1263,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); - $authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $authDuration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; $expire = DateTime::addSeconds(new \DateTime(), $authDuration); - $secret = Auth::tokenGenerator(); + $secret = $proofForToken->generate(); $session = new Document(array_merge([ '$id' => ID::unique(), '$permissions' => [ @@ -1268,9 +1275,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ], 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), - 'provider' => Auth::SESSION_PROVIDER_EMAIL, + 'provider' => SESSION_PROVIDER_EMAIL, 'providerUid' => $user->getAttribute('email'), - 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak + 'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), 'factors' => ['email'], @@ -1282,14 +1289,19 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') Authorization::setRole(Role::user($userId)->toString()); + $encoded = $store + ->setProperty('id', $user->getId()) + ->setProperty('secret', $secret) + ->encode(); + if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } $response ->addCookie( - name: Auth::$cookieName . '_legacy', - value: Auth::encodeSession($user->getId(), $secret), + name: $store->getKey() . '_legacy', + value: $encoded, expire: (new \DateTime($expire))->getTimestamp(), path: '/', domain: Config::getParam('cookieDomain'), @@ -1297,8 +1309,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') httponly: true ) ->addCookie( - name: Auth::$cookieName, - value: Auth::encodeSession($user->getId(), $secret), + name: $store->getKey(), + value: $encoded, expire: (new \DateTime($expire))->getTimestamp(), path: '/', domain: Config::getParam('cookieDomain'), diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 3fe949b8cb..d79c3f1e47 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -1,7 +1,6 @@ getAttribute('auths', [])['passwordHistory'] ?? 0; if (!empty($email)) { @@ -104,8 +114,24 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e } catch (Throwable) { $emailCanonical = null; } + $hashedPassword = null; + + $isHashed = !$hash instanceof Plaintext; + + $defaultHash = new ProofsPassword(); + if (!empty($password)) { + if (!$isHashed) { // Password was never hashed, hash it with the default hash + $hashedPassword = $defaultHash->hash($password); + $hash = $defaultHash->getHash(); + } else { + $hashedPassword = $password; + } + } else { + // when password is not provided, plaintext was set as the default hash causing the issue + $hash = $defaultHash->getHash(); + $isHashed = !$hash instanceof Plaintext; + } - $password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null; $user = new Document([ '$id' => $userId, '$permissions' => [ @@ -119,11 +145,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e 'phoneVerification' => false, 'status' => true, 'labels' => [], - 'password' => $password, - 'passwordHistory' => is_null($password) || $passwordHistory === 0 ? [] : [$password], - 'passwordUpdate' => (!empty($password)) ? DateTime::now() : null, - 'hash' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO : $hash, - 'hashOptions' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO_OPTIONS : $hashOptionsObject + ['type' => $hash], + 'password' => $hashedPassword, + 'passwordHistory' => is_null($hashedPassword) || $passwordHistory === 0 ? [] : [$hashedPassword], + 'passwordUpdate' => (!empty($hashedPassword)) ? DateTime::now() : null, + 'hash' => $hash->getName(), + 'hashOptions' => $hash->getOptions(), 'registration' => DateTime::now(), 'reset' => false, 'name' => $name, @@ -139,7 +165,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e 'emailIsFree' => $emailCanonical?->isFree(), ]); - if ($hash === 'plaintext') { + if (!$isHashed && !empty($password)) { $hooks->trigger('passwordValidator', [$dbForProject, $project, $plaintextPassword, &$user, true]); } @@ -230,7 +256,9 @@ App::post('/v1/users') ->inject('dbForProject') ->inject('hooks') ->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { - $user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks); + $plaintext = new Plaintext(); + + $user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($user, Response::MODEL_USER); @@ -264,7 +292,10 @@ App::post('/v1/users/bcrypt') ->inject('dbForProject') ->inject('hooks') ->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { - $user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $bcrypt = new Bcrypt(); + $bcrypt->setCost(8); // Default cost + + $user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -299,7 +330,9 @@ App::post('/v1/users/md5') ->inject('dbForProject') ->inject('hooks') ->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { - $user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $md5 = new MD5(); + + $user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -334,7 +367,9 @@ App::post('/v1/users/argon2') ->inject('dbForProject') ->inject('hooks') ->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { - $user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $argon2 = new Argon2(); + + $user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -370,13 +405,12 @@ App::post('/v1/users/sha') ->inject('dbForProject') ->inject('hooks') ->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { - $options = '{}'; - + $sha = new Sha(); if (!empty($passwordVersion)) { - $options = '{"version":"' . $passwordVersion . '"}'; + $sha->setVersion($passwordVersion); } - $user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -411,7 +445,9 @@ App::post('/v1/users/phpass') ->inject('dbForProject') ->inject('hooks') ->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { - $user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $phpass = new PHPass(); + + $user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -451,15 +487,15 @@ App::post('/v1/users/scrypt') ->inject('dbForProject') ->inject('hooks') ->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { - $options = [ - 'salt' => $passwordSalt, - 'costCpu' => $passwordCpu, - 'costMemory' => $passwordMemory, - 'costParallel' => $passwordParallel, - 'length' => $passwordLength - ]; + $scrypt = new Scrypt(); + $scrypt + ->setSalt($passwordSalt) + ->setCpuCost($passwordCpu) + ->setMemoryCost($passwordMemory) + ->setParallelCost($passwordParallel) + ->setLength($passwordLength); - $user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -497,7 +533,13 @@ App::post('/v1/users/scrypt-modified') ->inject('dbForProject') ->inject('hooks') ->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) { - $user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); + $scryptModified = new ScryptModified(); + $scryptModified + ->setSalt($passwordSalt) + ->setSaltSeparator($passwordSaltSeparator) + ->setSignerKey($passwordSignerKey); + + $user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -809,16 +851,12 @@ App::get('/v1/users/:userId/sessions') if ($user->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } - $sessions = $user->getAttribute('sessions', []); - foreach ($sessions as $key => $session) { /** @var Document $session */ - $countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')); $session->setAttribute('countryName', $countryName); $session->setAttribute('current', false); - $sessions[$key] = $session; } @@ -858,27 +896,22 @@ App::get('/v1/users/:userId/memberships') if ($user->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } - try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } - if (!empty($search)) { $queries[] = Query::search('search', $search); } - // Set internal queries $queries[] = Query::equal('userInternalId', [$user->getSequence()]); - $memberships = array_map(function ($membership) use ($dbForProject, $user) { $team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId')); $membership ->setAttribute('teamName', $team->getAttribute('name')) ->setAttribute('userName', $user->getAttribute('name')) ->setAttribute('userEmail', $user->getAttribute('email')); - return $membership; }, $dbForProject->find('memberships', $queries)); @@ -919,35 +952,26 @@ App::get('/v1/users/:userId/logs') if ($user->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } - try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } - // Temp fix for logs $queries[] = Query::or([ Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))), Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))), ]); - $audit = new Audit($dbForProject); - $logs = $audit->getLogsByUser($user->getSequence(), $queries); - $output = []; - foreach ($logs as $i => &$log) { $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; - $detector = new Detector($log['userAgent']); $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - $os = $detector->getOS(); $client = $detector->getClient(); $device = $detector->getDevice(); - $output[$i] = new Document([ 'event' => $log['event'], 'userId' => ID::custom($log['data']['userId']), @@ -968,9 +992,7 @@ App::get('/v1/users/:userId/logs') 'deviceBrand' => $device['deviceBrand'], 'deviceModel' => $device['deviceModel'] ]); - $record = $geodb->get($log['ip']); - if ($record) { $output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--'; $output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown')); @@ -1014,15 +1036,12 @@ App::get('/v1/users/:userId/targets') if ($user->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } - try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } - $queries[] = Query::equal('userId', [$userId]); - /** * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries */ @@ -1030,20 +1049,16 @@ App::get('/v1/users/:userId/targets') return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); }); $cursor = reset($cursor); - if ($cursor) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); } - $targetId = $cursor->getValue(); $cursorDocument = $dbForProject->getDocument('targets', $targetId); - if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Target '{$targetId}' for the 'cursor' value not found."); } - $cursor->setValue($cursorDocument); } try { @@ -1091,7 +1106,6 @@ App::get('/v1/users/identities') if (!empty($search)) { $queries[] = Query::search('search', $search); } - /** * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries */ @@ -1101,19 +1115,15 @@ App::get('/v1/users/identities') $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ - $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); } - $identityId = $cursor->getValue(); $cursorDocument = $dbForProject->getDocument('identities', $identityId); - if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "User '{$identityId}' for the 'cursor' value not found."); } - $cursor->setValue($cursorDocument); } @@ -1353,12 +1363,17 @@ App::patch('/v1/users/:userId/password') $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]); - $newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); + // Create Argon2 hasher with default settings + $hasher = new Argon2(); + $newPassword = $hasher->hash($password); + + $hash = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions')); $historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0; $history = $user->getAttribute('passwordHistory', []); + if ($historyLimit > 0) { - $validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions')); + $validator = new PasswordHistory($history, $hash); if (!$validator->isValid($password)) { throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED); } @@ -1371,8 +1386,8 @@ App::patch('/v1/users/:userId/password') ->setAttribute('password', $newPassword) ->setAttribute('passwordHistory', $history) ->setAttribute('passwordUpdate', DateTime::now()) - ->setAttribute('hash', Auth::DEFAULT_ALGO) - ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS); + ->setAttribute('hash', $hasher->getName()) + ->setAttribute('hashOptions', $hasher->getOptions()); $user = $dbForProject->updateDocument('users', $user->getId(), $user); @@ -2196,17 +2211,19 @@ App::post('/v1/users/:userId/sessions') ->inject('locale') ->inject('geodb') ->inject('queueForEvents') - ->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) { + ->inject('store') + ->inject('proofForToken') + ->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) { $user = $dbForProject->getDocument('users', $userId); if ($user->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } - $secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION); + $secret = $proofForToken->generate(); $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); - $duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $session = new Document(array_merge( @@ -2214,8 +2231,8 @@ App::post('/v1/users/:userId/sessions') '$id' => ID::unique(), 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), - 'provider' => Auth::SESSION_PROVIDER_SERVER, - 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak + 'provider' => SESSION_PROVIDER_SERVER, + 'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak 'userAgent' => $request->getUserAgent('UNKNOWN'), 'factors' => ['server'], 'ip' => $request->getIP(), @@ -2239,8 +2256,13 @@ App::post('/v1/users/:userId/sessions') $dbForProject->purgeCachedDocument('users', $user->getId()); + $encoded = $store + ->setProperty('id', $user->getId()) + ->setProperty('secret', $secret) + ->encode(); + $session - ->setAttribute('secret', Auth::encodeSession($user->getId(), $secret)) + ->setAttribute('secret', $encoded) ->setAttribute('countryName', $countryName); $queueForEvents @@ -2275,7 +2297,7 @@ App::post('/v1/users/:userId/tokens') )) ->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject']) ->param('length', 6, new Range(4, 128), 'Token length in characters. The default length is 6 characters', true) - ->param('expire', Auth::TOKEN_EXPIRATION_GENERIC, new Range(60, Auth::TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true) + ->param('expire', TOKEN_EXPIRATION_GENERIC, new Range(60, TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true) ->inject('request') ->inject('response') ->inject('dbForProject') @@ -2287,15 +2309,17 @@ App::post('/v1/users/:userId/tokens') throw new Exception(Exception::USER_NOT_FOUND); } - $secret = Auth::tokenGenerator($length); + $proofForToken = new Token($length); + $proofForToken->setHash(new Sha()); + $secret = $proofForToken->generate(); $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $expire)); $token = new Document([ '$id' => ID::unique(), 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), - 'type' => Auth::TOKEN_TYPE_GENERIC, - 'secret' => Auth::hash($secret), + 'type' => TOKEN_TYPE_GENERIC, + 'secret' => $proofForToken->hash($secret), 'expire' => $expire, 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP() @@ -2608,7 +2632,8 @@ App::post('/v1/users/:userId/jwts') $session = \count($sessions) > 0 ? $sessions[\count($sessions) - 1] : new Document(); } else { // Find by ID - foreach ($sessions as $loopSession) { /** @var Utopia\Database\Document $loopSession */ + foreach ($sessions as $loopSession) { + /** @var Utopia\Database\Document $loopSession */ if ($loopSession->getId() == $sessionId) { $session = $loopSession; break; diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 204957f485..c424e6884f 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -31,7 +31,10 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Query\Cursor; +use Utopia\Database\Validator\Query\Limit; +use Utopia\Database\Validator\Query\Offset; use Utopia\Detector\Detection\Framework\Analog; use Utopia\Detector\Detection\Framework\Angular; use Utopia\Detector\Detection\Framework\Astro; @@ -1033,10 +1036,11 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories') ->param('installationId', '', new Text(256), 'Installation Id') ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) ->inject('gitHub') ->inject('response') ->inject('dbForPlatform') - ->action(function (string $installationId, string $type, string $search, GitHub $github, Response $response, Database $dbForPlatform) { + ->action(function (string $installationId, string $type, string $search, array $queries, GitHub $github, Response $response, Database $dbForPlatform) { if (empty($search)) { $search = ""; } @@ -1052,11 +1056,20 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories') $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - $page = 1; - $perPage = 4; + $queries = Query::parseQueries($queries); + $limitQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_LIMIT)); + $offsetQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_OFFSET)); + $limit = !empty($limitQuery) ? $limitQuery->getValue() : 4; + $offset = !empty($offsetQuery) ? $offsetQuery->getValue() : 0; + + if ($offset % $limit !== 0) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'offset must be a multiple of the limit'); + } + + $page = ($offset / $limit) + 1; $owner = $github->getOwnerName($providerInstallationId); - $repos = $github->searchRepositories($owner, $page, $perPage, $search); + ['items' => $repos, 'total' => $total] = $github->searchRepositories($owner, $page, $limit, $search); $repos = \array_map(function ($repo) use ($installation) { $repo['id'] = \strval($repo['id'] ?? ''); @@ -1228,7 +1241,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories') $response->dynamic(new Document([ $type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos, - 'total' => \count($repos), + 'total' => $total, ]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST); }); @@ -1783,7 +1796,8 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor throw new Exception(Exception::INSTALLATION_NOT_FOUND); } - $repository = Authorization::skip(fn () => $dbForPlatform->getDocument('repositories', $repositoryId, [ + $repository = Authorization::skip(fn () => $dbForPlatform->findOne('repositories', [ + Query::equal('$id', [$repositoryId]), Query::equal('projectInternalId', [$project->getSequence()]) ])); diff --git a/app/controllers/general.php b/app/controllers/general.php index e0435cd499..f034da6b24 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -4,7 +4,6 @@ require_once __DIR__ . '/../init.php'; use Ahc\Jwt\JWT; use Ahc\Jwt\JWTException; -use Appwrite\Auth\Auth; use Appwrite\Auth\Key; use Appwrite\Event\Certificate; use Appwrite\Event\Event; @@ -17,6 +16,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Transformation\Adapter\Preview; use Appwrite\Transformation\Transformation; +use Appwrite\Utopia\Database\Documents\User as DBUser; use Appwrite\Utopia\Request; use Appwrite\Utopia\Request\Filters\V16 as RequestV16; use Appwrite\Utopia\Request\Filters\V17 as RequestV17; @@ -223,7 +223,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw */ $requirePreview = \is_null($apiKey) || !$apiKey->isPreviewAuthDisabled(); if ($isPreview && $requirePreview) { - $cookie = $request->getCookie(Auth::$cookieNamePreview, ''); + $cookie = $request->getCookie(COOKIE_NAME_PREVIEW, ''); $authorized = false; // Security checks to mark authorized true @@ -1260,7 +1260,7 @@ App::error() * If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php */ if (!$publish && $project->getId() !== 'console') { - if (!Auth::isPrivilegedUser(Authorization::getRoles())) { + if (!DBUser::isPrivileged(Authorization::getRoles())) { $fileSize = 0; $file = $request->getFiles('file'); if (!empty($file)) { @@ -1617,7 +1617,7 @@ App::get('/_appwrite/authorize') $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $response - ->addCookie(Auth::$cookieNamePreview, $jwt, (new \DateTime($expire))->getTimestamp(), '/', $host, ('https' === $protocol), true, null) + ->addCookie(COOKIE_NAME_PREVIEW, $jwt, (new \DateTime($expire))->getTimestamp(), '/', $host, ('https' === $protocol), true, null) ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') ->addHeader('Pragma', 'no-cache') ->redirect($protocol . '://' . $host . $path); diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index e571eb1848..298322671e 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -1,6 +1,5 @@ $match) { $find = $matches[0][$pos]; @@ -232,44 +231,97 @@ App::init() ->inject('mode') ->inject('team') ->inject('apiKey') - ->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) { + ->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) { $route = $utopia->getRoute(); - if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted' && str_starts_with($route->getPath(), '/v1/backups')) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Database Backups are available on Appwrite Cloud'); - } + + /** + * Handle user authentication and session validation. + * + * This function follows a series of steps to determine the appropriate user session + * based on cookies, headers, and JWT tokens. + * + * Process: + * + * Project & Role Validation: + * 1. Check if the project is empty. If so, throw an exception. + * 2. Get the roles configuration. + * 3. Determine the role for the user based on the user document. + * 4. Get the scopes for the role. + * + * API Key Authentication: + * 5. If there is an API key: + * - Verify no user session exists simultaneously + * - Check if key is expired + * - Set role and scopes from API key + * - Handle special app role case + * - For standard keys, update last accessed time + * + * User Activity: + * 6. If the project is not the console and user is not admin: + * - Update user's last activity timestamp + * + * Access Control: + * 7. Get the method from the route + * 8. Validate namespace permissions + * 9. Validate scope permissions + * 10. Check if user is blocked + * + * Security Checks: + * 11. Verify password status (check if reset required) + * 12. Validate MFA requirements: + * - Check if MFA is enabled + * - Verify email status + * - Verify phone status + * - Verify authenticator status + * 13. Handle Multi-Factor Authentication: + * - Check remaining required factors + * - Validate factor completion + * - Throw exception if factors incomplete + */ + + // Step 1: Check if project is empty if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } + // Step 2: Get roles configuration $roles = Config::getParam('roles', []); + // Step 3: Determine role for user + // TODO get scopes from the identity instead of the user roles config. The identity will containn the scopes the user authorized for the access token. + $role = $user->isEmpty() ? Role::guests()->toString() : Role::users()->toString(); + // Step 4: Get scopes for the role $scopes = $roles[$role]['scopes']; - // API Key authentication + // Step 5: API Key Authentication if (!empty($apiKey)) { + // Verify no user session exists simultaneously if (!$user->isEmpty()) { throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET); } + // Check if key is expired if ($apiKey->isExpired()) { throw new Exception(Exception::PROJECT_KEY_EXPIRED); } + // Set role and scopes from API key $role = $apiKey->getRole(); $scopes = $apiKey->getScopes(); - if ($apiKey->getRole() === Auth::USER_ROLE_APPS) { + // Handle special app role case + if ($apiKey->getRole() === User::ROLE_APPS) { // Disable authorization checks for API keys Authorization::setDefaultStatus(false); - $user = new Document([ + $user = new User([ '$id' => '', 'status' => true, - 'type' => Auth::ACTIVITY_TYPE_APP, + 'type' => ACTIVITY_TYPE_APP, 'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(), 'password' => '', 'name' => $apiKey->getName(), @@ -278,6 +330,7 @@ App::init() $queueForAudits->setUser($user); } + // For standard keys, update last accessed time if ($apiKey->getType() === API_KEY_STANDARD) { $dbKey = $project->find( key: 'secret', @@ -343,11 +396,11 @@ App::init() $scopes = \array_unique($scopes); Authorization::setRole($role); - foreach (Auth::getRoles($user) as $authRole) { + foreach ($user->getRoles() as $authRole) { Authorization::setRole($authRole); } - // Update project last activity + // Step 6: Update project and user last activity if (!$project->isEmpty() && $project->getId() !== 'console') { $accessedAt = $project->getAttribute('accessedAt', 0); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { @@ -356,7 +409,6 @@ App::init() } } - // Update user last activity if (!empty($user->getId())) { $accessedAt = $user->getAttribute('accessedAt', 0); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) { @@ -370,6 +422,7 @@ App::init() } } + // Steps 7-9: Access Control - Method, Namespace and Scope Validation /** * @var ?Method $method */ @@ -387,27 +440,29 @@ App::init() if ( array_key_exists($namespace, $project->getAttribute('services', [])) && !$project->getAttribute('services', [])[$namespace] - && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + && !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles())) ) { throw new Exception(Exception::GENERAL_SERVICE_DISABLED); } } - // Do now allow access if scope is not allowed + // Step 9: Validate scope permissions $allowed = (array)$route->getLabel('scope', 'none'); if (empty(\array_intersect($allowed, $scopes))) { throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scopes (' . \json_encode($allowed) . ')'); } - // Do not allow access to blocked accounts + // Step 10: Check if user is blocked if (false === $user->getAttribute('status')) { // Account is blocked throw new Exception(Exception::USER_BLOCKED); } + // Step 11: Verify password status if ($user->getAttribute('reset')) { throw new Exception(Exception::USER_PASSWORD_RESET_REQUIRED); } + // Step 12: Validate MFA requirements $mfaEnabled = $user->getAttribute('mfa', false); $hasVerifiedEmail = $user->getAttribute('emailVerification', false); $hasVerifiedPhone = $user->getAttribute('phoneVerification', false); @@ -415,6 +470,7 @@ App::init() $hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator; $minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1; + // Step 13: Handle Multi-Factor Authentication if (!in_array('mfa', $route->getGroups())) { if ($session && \count($session->getAttribute('factors', [])) < $minimumFactors) { throw new Exception(Exception::USER_MORE_FACTORS_REQUIRED); @@ -454,7 +510,7 @@ App::init() if ( array_key_exists('rest', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['rest'] - && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + && !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } @@ -486,8 +542,8 @@ App::init() $closestLimit = null; $roles = Authorization::getRoles(); - $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); + $isPrivilegedUser = User::isPrivileged($roles); + $isAppUser = User::isApp($roles); foreach ($timeLimitArray as $timeLimit) { foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys @@ -543,7 +599,7 @@ App::init() if (!$user->isEmpty()) { $userClone = clone $user; // $user doesn't support `type` and can cause unintended effects. - $userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER); + $userClone->setAttribute('type', ACTIVITY_TYPE_USER); $queueForAudits->setUser($userClone); } @@ -586,7 +642,7 @@ App::init() if ($useCache) { $route = $utopia->match($request); $isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview'; - $isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !Auth::isPrivilegedUser(Authorization::getRoles()); + $isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged(Authorization::getRoles()); $key = $request->cacheIdentifier(); $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); @@ -609,7 +665,7 @@ App::init() $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence(); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); @@ -643,7 +699,7 @@ App::init() throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } //Do not update transformedAt if it's a console user - if (!Auth::isPrivilegedUser(Authorization::getRoles())) { + if (!User::isPrivileged(Authorization::getRoles())) { $transformedAt = $file->getAttribute('transformedAt', ''); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) { $file->setAttribute('transformedAt', DateTime::now()); @@ -743,7 +799,7 @@ App::shutdown() ->inject('queueForWebhooks') ->inject('queueForRealtime') ->inject('dbForProject') - ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) { + ->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) { $responsePayload = $response->getPayload(); @@ -791,7 +847,7 @@ App::shutdown() if (!$user->isEmpty()) { $userClone = clone $user; // $user doesn't support `type` and can cause unintended effects. - $userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER); + $userClone->setAttribute('type', ACTIVITY_TYPE_USER); $queueForAudits->setUser($userClone); } elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) { /** @@ -802,10 +858,10 @@ App::shutdown() * * Therefore, we consider this an anonymous request and create a relevant user. */ - $user = new Document([ + $user = new User([ '$id' => '', 'status' => true, - 'type' => Auth::ACTIVITY_TYPE_GUEST, + 'type' => ACTIVITY_TYPE_GUEST, 'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(), 'password' => '', 'name' => 'Guest', @@ -895,7 +951,7 @@ App::shutdown() } if ($project->getId() !== 'console') { - if (!Auth::isPrivilegedUser(Authorization::getRoles())) { + if (!User::isPrivileged(Authorization::getRoles())) { $fileSize = 0; $file = $request->getFiles('file'); if (!empty($file)) { diff --git a/app/controllers/shared/api/auth.php b/app/controllers/shared/api/auth.php index ecabc641ec..efa733fc34 100644 --- a/app/controllers/shared/api/auth.php +++ b/app/controllers/shared/api/auth.php @@ -1,7 +1,7 @@ getAttribute('mfaUpdatedAt'); if (!empty($lastUpdate)) { $now = DateTime::now(); - $maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), Auth::MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge + $maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge $isSessionFresh = DateTime::formatTz($maxAllowedDate) >= DateTime::formatTz($now); } @@ -49,8 +49,8 @@ App::init() $route = $utopia->match($request); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - $isAppUser = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); + $isAppUser = User::isApp(Authorization::getRoles()); if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs return; diff --git a/app/init/constants.php b/app/init/constants.php index 6c0058f9b0..244d4441a9 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -93,6 +93,69 @@ const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io'; const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite'; const APP_BRANDED_EMAIL_BASE_TEMPLATE = 'email-base-styled'; +/** + * JWT for Resource Tokens. + */ +const RESOURCE_TOKEN_ALGORITHM = 'HS256'; +const RESOURCE_TOKEN_MAX_AGE = 86400 * 365 * 10; /* 10 years */ +const RESOURCE_TOKEN_LEEWAY = 10; // 10 seconds + +/** + * Token Expiration times. + */ +const TOKEN_EXPIRATION_LOGIN_LONG = 31536000; /* 1 year */ +const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */ +const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */ +const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */ +const TOKEN_EXPIRATION_OTP = 60 * 15; /* 15 minutes */ +const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */ + +/** + * Token Lengths. + */ +const TOKEN_LENGTH_MAGIC_URL = 64; +const TOKEN_LENGTH_VERIFICATION = 256; +const TOKEN_LENGTH_RECOVERY = 256; +const TOKEN_LENGTH_OAUTH2 = 64; +const TOKEN_LENGTH_SESSION = 256; + +/** + * Token Types. + */ +const TOKEN_TYPE_LOGIN = 1; // Deprecated +const TOKEN_TYPE_VERIFICATION = 2; +const TOKEN_TYPE_RECOVERY = 3; +const TOKEN_TYPE_INVITE = 4; +const TOKEN_TYPE_MAGIC_URL = 5; +const TOKEN_TYPE_PHONE = 6; +const TOKEN_TYPE_OAUTH2 = 7; +const TOKEN_TYPE_GENERIC = 8; +const TOKEN_TYPE_EMAIL = 9; // OTP + +/** + * Session Providers. + */ +const SESSION_PROVIDER_EMAIL = 'email'; +const SESSION_PROVIDER_ANONYMOUS = 'anonymous'; +const SESSION_PROVIDER_MAGIC_URL = 'magic-url'; +const SESSION_PROVIDER_PHONE = 'phone'; +const SESSION_PROVIDER_OAUTH2 = 'oauth2'; +const SESSION_PROVIDER_TOKEN = 'token'; +const SESSION_PROVIDER_SERVER = 'server'; + +/** + * Activity associated with user or the app. + */ +const ACTIVITY_TYPE_APP = 'app'; +const ACTIVITY_TYPE_USER = 'user'; +const ACTIVITY_TYPE_GUEST = 'guest'; + +/** + * MFA + */ +const MFA_RECENT_DURATION = 1800; // 30 mins + + // Database Reconnect const DATABASE_RECONNECT_SLEEP = 2; const DATABASE_RECONNECT_MAX_ATTEMPTS = 10; @@ -302,6 +365,9 @@ const SCHEDULE_RESOURCE_TYPE_EXECUTION = 'execution'; const SCHEDULE_RESOURCE_TYPE_FUNCTION = 'function'; const SCHEDULE_RESOURCE_TYPE_MESSAGE = 'message'; +/** Preview cookie */ +const COOKIE_NAME_PREVIEW = 'a_jwt_console'; + // Database types const DATABASE_LEGACY_TYPE = 'legacy'; const DATABASE_TABLESDB_TYPE = 'tablesdb'; diff --git a/app/init/registers.php b/app/init/registers.php index d9dfa5e7ab..eadd31527d 100644 --- a/app/init/registers.php +++ b/app/init/registers.php @@ -42,6 +42,7 @@ if (!App::isProduction()) { PublicDomain::allow(['request-catcher-sms']); PublicDomain::allow(['request-catcher-webhook']); } + $register->set('logger', function () { // Register error logger $providerName = System::getEnv('_APP_LOGGING_PROVIDER', ''); @@ -100,6 +101,51 @@ $register->set('logger', function () { return new Logger($adapter); }); +$register->set('realtimeLogger', function () { + // Register error logger for realtime, falls back to default logging config + $providerConfig = System::getEnv('_APP_LOGGING_CONFIG_REALTIME', '') + ?: System::getEnv('_APP_LOGGING_CONFIG', ''); + + if (empty($providerConfig)) { + return; + } + + $loggingProvider = new DSN($providerConfig); + $providerName = $loggingProvider->getScheme(); + $providerConfig = match ($providerName) { + 'sentry' => ['key' => $loggingProvider->getPassword(), 'projectId' => $loggingProvider->getUser() ?? '', 'host' => 'https://' . $loggingProvider->getHost()], + 'logowl' => ['ticket' => $loggingProvider->getUser() ?? '', 'host' => $loggingProvider->getHost()], + default => ['key' => $loggingProvider->getHost()], + }; + + if (empty($providerName) || empty($providerConfig)) { + return; + } + + if (!Logger::hasProvider($providerName)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "Logging provider not supported. Logging is disabled"); + } + + try { + $adapter = match ($providerName) { + 'sentry' => new Sentry($providerConfig['projectId'], $providerConfig['key'], $providerConfig['host']), + 'logowl' => new LogOwl($providerConfig['ticket'], $providerConfig['host']), + 'raygun' => new Raygun($providerConfig['key']), + 'appsignal' => new AppSignal($providerConfig['key']), + default => null + }; + } catch (Throwable $th) { + $adapter = null; + } + + if ($adapter === null) { + Console::error("Logging provider not supported. Logging is disabled"); + return; + } + + return new Logger($adapter); +}); + $register->set('pools', function () { $group = new Group(); @@ -398,7 +444,7 @@ $register->set('smtp', function () { return $mail; }); $register->set('geodb', function () { - return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2024-09.mmdb'); + return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb'); }); $register->set('passwordsDictionary', function () { $content = \file_get_contents(__DIR__ . '/../assets/security/10k-common-passwords'); diff --git a/app/init/resources.php b/app/init/resources.php index a7e28ad376..6750804e13 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -2,7 +2,6 @@ use Ahc\Jwt\JWT; use Ahc\Jwt\JWTException; -use Appwrite\Auth\Auth; use Appwrite\Auth\Key; use Appwrite\Databases\TransactionState; use Appwrite\Event\Audit; @@ -23,12 +22,20 @@ use Appwrite\Extend\Exception; use Appwrite\GraphQL\Schema; use Appwrite\Network\Platform; use Appwrite\Network\Validator\Origin; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Request; +use Appwrite\Utopia\Response; use Executor\Executor; use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis; use Utopia\Agents\Adapters\Ollama; use Utopia\Agents\Agent; use Utopia\App; +use Utopia\Auth\Hashes\Argon2; +use Utopia\Auth\Hashes\Sha; +use Utopia\Auth\Proofs\Code; +use Utopia\Auth\Proofs\Password; +use Utopia\Auth\Proofs\Token; +use Utopia\Auth\Store; use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\Cache\Adapter\Sharding; use Utopia\Cache\Cache; @@ -228,76 +235,91 @@ App::setResource('platforms', function (Request $request, Document $console, Doc ]; }, ['request', 'console', 'project', 'dbForPlatform']); -App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForPlatform) { - /** @var Appwrite\Utopia\Request $request */ - /** @var Appwrite\Utopia\Response $response */ - /** @var Utopia\Database\Document $project */ - /** @var Utopia\Database\Database $dbForProject */ - /** @var Utopia\Database\Database $dbForPlatform */ - /** @var string $mode */ +App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken) { + /** + * Handles user authentication and session validation. + * + * This function follows a series of steps to determine the appropriate user session + * based on cookies, headers, and JWT tokens. + * + * Process: + * 1. Checks the cookie based on mode: + * - If in admin mode, uses console project id for key. + * - Otherwise, sets the key using the project ID + * 2. If no cookie is found, attempts to retrieve the fallback header `x-fallback-cookies`. + * - If this method is used, returns the header: `X-Debug-Fallback: true`. + * 3. Fetches the user document from the appropriate database based on the mode. + * 4. If the user document is empty or the session key cannot be verified, sets an empty user document. + * 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token. + * 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`, + * overwriting the previous value. + */ Authorization::setDefaultStatus(true); - Auth::setCookieName('a_session_' . $project->getId()); + $store->setKey('a_session_' . $project->getId()); if (APP_MODE_ADMIN === $mode) { - Auth::setCookieName('a_session_' . $console->getId()); + $store->setKey('a_session_' . $console->getId()); } - $session = Auth::decodeSession( + $store->decode( $request->getCookie( - Auth::$cookieName, // Get sessions - $request->getCookie(Auth::$cookieName . '_legacy', '') + $store->getKey(), // Get sessions + $request->getCookie($store->getKey() . '_legacy', '') ) ); // Get session from header for SSR clients - if (empty($session['id']) && empty($session['secret'])) { + if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) { $sessionHeader = $request->getHeader('x-appwrite-session', ''); if (!empty($sessionHeader)) { - $session = Auth::decodeSession($sessionHeader); + $store->decode($sessionHeader); } } // Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies - if ($response) { + if ($response) { // if in http context - add debug header $response->addHeader('X-Debug-Fallback', 'false'); } - if (empty($session['id']) && empty($session['secret'])) { + if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) { if ($response) { $response->addHeader('X-Debug-Fallback', 'true'); } $fallback = $request->getHeader('x-fallback-cookies', ''); $fallback = \json_decode($fallback, true); - $session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : '')); + $store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : '')); } - Auth::$unique = $session['id'] ?? ''; - Auth::$secret = $session['secret'] ?? ''; - - $user = new Document([]); - - if (!empty(Auth::$unique)) { - if ($mode === APP_MODE_ADMIN) { - $user = $dbForPlatform->getDocument('users', Auth::$unique); - } elseif (!$project->isEmpty()) { - if ($project->getId() === 'console') { - $user = $dbForPlatform->getDocument('users', Auth::$unique); - } else { - $user = $dbForProject->getDocument('users', Auth::$unique); + $user = null; + if (APP_MODE_ADMIN === $mode) { + /** @var User $user */ + $user = $dbForPlatform->getDocument('users', $store->getProperty('id', '')); + } else { + if ($project->isEmpty()) { + $user = new User([]); + } else { + if (!empty($store->getProperty('id', ''))) { + if ($project->getId() === 'console') { + /** @var User $user */ + $user = $dbForPlatform->getDocument('users', $store->getProperty('id', '')); + } else { + /** @var User $user */ + $user = $dbForProject->getDocument('users', $store->getProperty('id', '')); + } } } } if ( + !$user || $user->isEmpty() // Check a document has been found in the DB - || !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) + || !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken) ) { // Validate user has valid login token - $user = new Document([]); + $user = new User([]); } - // if (APP_MODE_ADMIN === $mode) { // if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) { // Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. @@ -305,18 +327,14 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons // $user = new Document([]); // } // } - $authJWT = $request->getHeader('x-appwrite-jwt', ''); - if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); - try { $payload = $jwt->decode($authJWT); } catch (JWTException $error) { throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage()); } - $jwtUserId = $payload['userId'] ?? ''; if (!empty($jwtUserId)) { if ($mode === APP_MODE_ADMIN) { @@ -325,11 +343,10 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons $user = $dbForProject->getDocument('users', $jwtUserId); } } - $jwtSessionId = $payload['sessionId'] ?? ''; if (!empty($jwtSessionId)) { if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token - $user = new Document([]); + $user = new User([]); } } } @@ -337,7 +354,7 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons $dbForPlatform->setMetadata('user', $user->getId()); return $user; -}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform']); +}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken']); App::setResource('project', function ($dbForPlatform, $request, $console) { /** @var Appwrite\Utopia\Request $request */ @@ -355,31 +372,61 @@ App::setResource('project', function ($dbForPlatform, $request, $console) { return $project; }, ['dbForPlatform', 'request', 'console']); -App::setResource('session', function (Document $user) { +App::setResource('session', function (User $user, Store $store, Token $proofForToken) { if ($user->isEmpty()) { return; } $sessions = $user->getAttribute('sessions', []); - $sessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret); + $sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken); if (!$sessionId) { return; } - - foreach ($sessions as $session) {/** @var Document $session */ + foreach ($sessions as $session) { + /** @var Document $session */ if ($sessionId === $session->getId()) { return $session; } } return; -}, ['user']); +}, ['user', 'store', 'proofForToken']); App::setResource('console', function () { return new Document(Config::getParam('console')); }, []); +App::setResource('store', function (): Store { + return new Store(); +}); + +App::setResource('proofForPassword', function (): Password { + $hash = new Argon2(); + $hash + ->setMemoryCost(7168) + ->setTimeCost(5) + ->setThreads(1); + + $password = new Password(); + $password + ->setHash($hash); + + return $password; +}); + +App::setResource('proofForToken', function (): Token { + $token = new Token(); + $token->setHash(new Sha()); + return $token; +}); + +App::setResource('proofForCode', function (): Code { + $code = new Code(); + $code->setHash(new Sha()); + return $code; +}); + App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project) { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForPlatform; @@ -400,6 +447,7 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform ->setMetadata('project', $project->getId()) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); + $database->setDocumentType('users', User::class); $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); @@ -429,6 +477,8 @@ App::setResource('dbForPlatform', function (Group $pools, Cache $cache) { ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); + $database->setDocumentType('users', User::class); + return $database; }, ['pools', 'cache']); @@ -573,6 +623,7 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform ->setMetadata('project', $project->getId()) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API) ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); + $database->setDocumentType('users', User::class); $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); @@ -1076,7 +1127,8 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) { $tokenJWT = $request->getParam('token'); if (!empty($tokenJWT) && !$project->isEmpty()) { // JWT authentication - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. + // Use a large but reasonable maxAge to avoid auto-exp when token has no expiry + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), RESOURCE_TOKEN_ALGORITHM, RESOURCE_TOKEN_MAX_AGE, RESOURCE_TOKEN_LEEWAY); // Instantiate with key, algo, maxAge and leeway. try { $payload = $jwt->decode($tokenJWT); diff --git a/app/realtime.php b/app/realtime.php index e18ab8e10d..3a68947cf6 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,11 +1,11 @@ setMetadata('host', \gethostname()) ->setMetadata('project', '_console'); + $database->setDocumentType('users', User::class); + return $database; } } @@ -118,6 +123,8 @@ if (!function_exists('getProjectDB')) { ->setMetadata('host', \gethostname()) ->setMetadata('project', $project->getId()); + $database->setDocumentType('users', User::class); + return $databases[$project->getSequence()] = $database; } } @@ -236,7 +243,7 @@ $adapter $server = new Server($adapter); $logError = function (Throwable $error, string $action) use ($register) { - $logger = $register->get('logger'); + $logger = $register->get('realtimeLogger'); if ($logger && !$error instanceof Exception) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); @@ -457,9 +464,10 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId)); $database = getProjectDB($project); + /** @var Appwrite\Utopia\Database\Documents\User $user */ $user = $database->getDocument('users', $userId); - $roles = Auth::getRoles($user); + $roles = $user->getRoles(); $channels = $realtime->connections[$connection]['channels']; $realtime->unsubscribe($connection); @@ -526,14 +534,14 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, if ( array_key_exists('realtime', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['realtime'] - && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) + && !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } $timelimit = $app->getResource('timelimit'); $platforms = $app->getResource('platforms'); - $user = $app->getResource('user'); /** @var Document $user */ + $user = $app->getResource('user'); /** @var User $user */ /* * Abuse Check @@ -563,7 +571,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription()); } - $roles = Auth::getRoles($user); + $roles = $user->getRoles(); $channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId()); @@ -678,21 +686,31 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.'); } - $session = Auth::decodeSession($message['data']['session']); - Auth::$unique = $session['id'] ?? ''; - Auth::$secret = $session['secret'] ?? ''; + $store = new Store(); - $user = $database->getDocument('users', Auth::$unique); + $store->decode($message['data']['session']); + + /** @var User $user */ + $user = $database->getDocument('users', $store->getProperty('id', '')); + + /** + * TODO: + * Moving forward, we should try to use our dependency injection container + * to inject the proof for token. + * This way we will have one source of truth for the proof for token. + */ + $proofForToken = new Token(); + $proofForToken->setHash(new Sha()); if ( empty($user->getId()) // Check a document has been found in the DB - || !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token + || !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken) // Validate user has valid login token ) { // cookie not valid throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.'); } - $roles = Auth::getRoles($user); + $roles = $user->getRoles(); $channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId()); $realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels); diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 1d4cdcd643..6b142a3305 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -870,7 +870,7 @@ $dbService = $this->getParam('database'); - _APP_DB_PASS appwrite-assistant: - image: appwrite/assistant:0.8.3 + image: appwrite/assistant:0.8.4 container_name: appwrite-assistant <<: *x-logging restart: unless-stopped @@ -878,9 +878,9 @@ $dbService = $this->getParam('database'); - appwrite environment: - _APP_ASSISTANT_OPENAI_API_KEY - + appwrite-browser: - image: appwrite/browser:0.3.1 + image: appwrite/browser:0.3.2 container_name: appwrite-browser <<: *x-logging restart: unless-stopped diff --git a/app/worker.php b/app/worker.php index 223f2db15f..97f6a1f12d 100644 --- a/app/worker.php +++ b/app/worker.php @@ -17,6 +17,7 @@ use Appwrite\Event\Realtime; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; use Appwrite\Platform\Appwrite; +use Appwrite\Utopia\Database\Documents\User; use Executor\Executor; use Swoole\Runtime; use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis; @@ -55,7 +56,7 @@ Server::setResource('dbForPlatform', function (Cache $cache, Registry $register) $adapter = new DatabasePool($pools->get('console')); $dbForPlatform = new Database($adapter, $cache); $dbForPlatform->setNamespace('_console'); - + $dbForPlatform->setDocumentType('users', User::class); return $dbForPlatform; }, ['cache', 'register']); @@ -86,6 +87,7 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register, $adapter = new DatabasePool($pools->get($dsn->getHost())); $database = new Database($adapter, $cache); + $database->setDocumentType('users', User::class); $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); diff --git a/composer.json b/composer.json index b6e7c6ebce..3a9d53496f 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,8 @@ "appwrite/php-clamav": "2.0.*", "utopia-php/abuse": "1.*", "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "1.0.*", + "utopia-php/audit": "1.*", + "utopia-php/auth": "0.5.*", "utopia-php/cache": "0.13.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "1.*.*", @@ -66,7 +67,7 @@ "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", - "utopia-php/migration": "1.*", + "utopia-php/migration": "1.4.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.8.*", @@ -77,7 +78,7 @@ "utopia-php/swoole": "0.8.*", "utopia-php/system": "0.9.*", "utopia-php/telemetry": "0.1.*", - "utopia-php/vcs": "0.12.*", + "utopia-php/vcs": "0.13.*", "utopia-php/websocket": "0.3.*", "matomo/device-detector": "6.1.*", "dragonmantank/cron-expression": "3.3.*", diff --git a/composer.lock b/composer.lock index 09d29b9418..2e6376b376 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b717375900f6871c875e1b61011ff8b5", + "content-hash": "c7d2810ea344ae2dcf2a7e1b66a54c18", "packages": [ { "name": "adhocore/jwt", @@ -283,16 +283,16 @@ }, { "name": "brick/math", - "version": "0.14.0", + "version": "0.14.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", "shasum": "" }, "require": { @@ -331,7 +331,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.0" + "source": "https://github.com/brick/math/tree/0.14.1" }, "funding": [ { @@ -339,7 +339,7 @@ "type": "github" } ], - "time": "2025-08-29T12:40:03+00:00" + "time": "2025-11-24T14:40:29+00:00" }, { "name": "chillerlan/php-qrcode", @@ -1231,16 +1231,16 @@ }, { "name": "open-telemetry/api", - "version": "1.7.0", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "610b79ad9d6d97e8368bcb6c4d42394fbb87b522" + "reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/610b79ad9d6d97e8368bcb6c4d42394fbb87b522", - "reference": "610b79ad9d6d97e8368bcb6c4d42394fbb87b522", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4", + "reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4", "shasum": "" }, "require": { @@ -1260,7 +1260,7 @@ ] }, "branch-alias": { - "dev-main": "1.7.x-dev" + "dev-main": "1.8.x-dev" } }, "autoload": { @@ -1293,11 +1293,11 @@ ], "support": { "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", - "docs": "https://opentelemetry.io/docs/php", + "docs": "https://opentelemetry.io/docs/languages/php", "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-10-02T23:44:28+00:00" + "time": "2025-10-19T10:49:48+00:00" }, { "name": "open-telemetry/context", @@ -1360,16 +1360,16 @@ }, { "name": "open-telemetry/exporter-otlp", - "version": "1.3.2", + "version": "1.3.3", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/exporter-otlp.git", - "reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2" + "reference": "07b02bc71838463f6edcc78d3485c04b48fb263d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/196f3a1dbce3b2c0f8110d164232c11ac00ddbb2", - "reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/07b02bc71838463f6edcc78d3485c04b48fb263d", + "reference": "07b02bc71838463f6edcc78d3485c04b48fb263d", "shasum": "" }, "require": { @@ -1416,11 +1416,11 @@ ], "support": { "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", - "docs": "https://opentelemetry.io/docs/php", + "docs": "https://opentelemetry.io/docs/languages/php", "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-06-16T00:24:51+00:00" + "time": "2025-11-13T08:04:37+00:00" }, { "name": "open-telemetry/gen-otlp-protobuf", @@ -1487,16 +1487,16 @@ }, { "name": "open-telemetry/sdk", - "version": "1.9.0", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e" + "reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e", - "reference": "8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99", + "reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99", "shasum": "" }, "require": { @@ -1576,11 +1576,11 @@ ], "support": { "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", - "docs": "https://opentelemetry.io/docs/php", + "docs": "https://opentelemetry.io/docs/languages/php", "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-10-02T23:44:28+00:00" + "time": "2025-11-25T10:59:15+00:00" }, { "name": "open-telemetry/sem-conv", @@ -1758,16 +1758,16 @@ }, { "name": "php-amqplib/php-amqplib", - "version": "v3.7.3", + "version": "v3.7.4", "source": { "type": "git", "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "9f50fe69a9f1a19e2cb25596a354d705de36fe59" + "reference": "381b6f7c600e0e0c7463cdd7f7a1a3bc6268e5fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/9f50fe69a9f1a19e2cb25596a354d705de36fe59", - "reference": "9f50fe69a9f1a19e2cb25596a354d705de36fe59", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/381b6f7c600e0e0c7463cdd7f7a1a3bc6268e5fd", + "reference": "381b6f7c600e0e0c7463cdd7f7a1a3bc6268e5fd", "shasum": "" }, "require": { @@ -1833,9 +1833,9 @@ ], "support": { "issues": "https://github.com/php-amqplib/php-amqplib/issues", - "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.7.3" + "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.7.4" }, - "time": "2025-02-18T20:11:13+00:00" + "time": "2025-11-23T17:00:56+00:00" }, { "name": "php-http/discovery", @@ -2668,16 +2668,16 @@ }, { "name": "symfony/http-client", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de" + "reference": "ee5e0e0139ab506f6063a230e631bed677c650a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de", - "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de", + "url": "https://api.github.com/repos/symfony/http-client/zipball/ee5e0e0139ab506f6063a230e631bed677c650a4", + "reference": "ee5e0e0139ab506f6063a230e631bed677c650a4", "shasum": "" }, "require": { @@ -2708,12 +2708,13 @@ "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2744,7 +2745,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.6" + "source": "https://github.com/symfony/http-client/tree/v7.4.0" }, "funding": [ { @@ -2764,7 +2765,7 @@ "type": "tidelift" } ], - "time": "2025-11-05T17:41:46+00:00" + "time": "2025-11-20T12:32:50+00:00" }, { "name": "symfony/http-client-contracts", @@ -3645,6 +3646,61 @@ }, "time": "2025-10-20T07:14:26+00:00" }, + { + "name": "utopia-php/auth", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/auth.git", + "reference": "5ad0ded3a79f153ee904b97b49f8dfe4669e4fd0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/auth/zipball/5ad0ded3a79f153ee904b97b49f8dfe4669e4fd0", + "reference": "5ad0ded3a79f153ee904b97b49f8dfe4669e4fd0", + "shasum": "" + }, + "require": { + "ext-hash": "*", + "ext-scrypt": "*", + "ext-sodium": "*", + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpstan/phpstan": "1.9.x-dev", + "phpunit/phpunit": "^9.3", + "vimeo/psalm": "4.0.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Auth\\": "src/Auth" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Utopia PHP", + "email": "team@appwrite.io" + } + ], + "description": "A simple PHP authentication library", + "keywords": [ + "Authentication", + "auth", + "php", + "security" + ], + "support": { + "issues": "https://github.com/utopia-php/auth/issues", + "source": "https://github.com/utopia-php/auth/tree/0.5.0" + }, + "time": "2025-10-29T07:11:43+00:00" + }, { "name": "utopia-php/cache", "version": "0.13.1", @@ -3947,16 +4003,16 @@ }, { "name": "utopia-php/detector", - "version": "0.2.2", + "version": "0.2.3", "source": { "type": "git", "url": "https://github.com/utopia-php/detector.git", - "reference": "9a41be5f21efe2d865de79b08dff94fff85ce5e9" + "reference": "c1f49b3e82250c3256ffba48aa9737d6c17a243a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/detector/zipball/9a41be5f21efe2d865de79b08dff94fff85ce5e9", - "reference": "9a41be5f21efe2d865de79b08dff94fff85ce5e9", + "url": "https://api.github.com/repos/utopia-php/detector/zipball/c1f49b3e82250c3256ffba48aa9737d6c17a243a", + "reference": "c1f49b3e82250c3256ffba48aa9737d6c17a243a", "shasum": "" }, "require": { @@ -3986,22 +4042,22 @@ ], "support": { "issues": "https://github.com/utopia-php/detector/issues", - "source": "https://github.com/utopia-php/detector/tree/0.2.2" + "source": "https://github.com/utopia-php/detector/tree/0.2.3" }, - "time": "2025-10-31T12:43:31+00:00" + "time": "2025-11-24T15:52:51+00:00" }, { "name": "utopia-php/dns", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/utopia-php/dns.git", - "reference": "1e6b4bac735329c9e5ec69a6a5d899ec2d050707" + "reference": "eea6b9299a1420ae6c574f16eb1e9da8689ac56b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/dns/zipball/1e6b4bac735329c9e5ec69a6a5d899ec2d050707", - "reference": "1e6b4bac735329c9e5ec69a6a5d899ec2d050707", + "url": "https://api.github.com/repos/utopia-php/dns/zipball/eea6b9299a1420ae6c574f16eb1e9da8689ac56b", + "reference": "eea6b9299a1420ae6c574f16eb1e9da8689ac56b", "shasum": "" }, "require": { @@ -4009,7 +4065,7 @@ "utopia-php/console": "0.0.*", "utopia-php/domains": "0.9.*", "utopia-php/telemetry": "0.1.*", - "utopia-php/validators": "^0.0.2" + "utopia-php/validators": "0.*" }, "require-dev": { "laravel/pint": "1.25.*", @@ -4043,32 +4099,32 @@ ], "support": { "issues": "https://github.com/utopia-php/dns/issues", - "source": "https://github.com/utopia-php/dns/tree/1.1.3" + "source": "https://github.com/utopia-php/dns/tree/1.1.4" }, - "time": "2025-11-06T19:08:29+00:00" + "time": "2025-11-26T13:38:10+00:00" }, { "name": "utopia-php/domains", - "version": "0.9.1", + "version": "0.9.2", "source": { "type": "git", "url": "https://github.com/utopia-php/domains.git", - "reference": "99b4ec95d5d6b7a5c990a66c56412212d9af37e7" + "reference": "52b654f8a0e170bfa2e54cb47755b256822477c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/domains/zipball/99b4ec95d5d6b7a5c990a66c56412212d9af37e7", - "reference": "99b4ec95d5d6b7a5c990a66c56412212d9af37e7", + "url": "https://api.github.com/repos/utopia-php/domains/zipball/52b654f8a0e170bfa2e54cb47755b256822477c7", + "reference": "52b654f8a0e170bfa2e54cb47755b256822477c7", "shasum": "" }, "require": { - "php": ">=8.0", + "php": ">=8.2", "utopia-php/cache": "0.13.*", - "utopia-php/validators": "0.0.*" + "utopia-php/validators": "0.*" }, "require-dev": { - "laravel/pint": "1.2.*", - "phpstan/phpstan": "1.9.x-dev", + "laravel/pint": "^1.18", + "phpstan/phpstan": "^1.12", "phpunit/phpunit": "^9.3" }, "type": "library", @@ -4105,9 +4161,9 @@ ], "support": { "issues": "https://github.com/utopia-php/domains/issues", - "source": "https://github.com/utopia-php/domains/tree/0.9.1" + "source": "https://github.com/utopia-php/domains/tree/0.9.2" }, - "time": "2025-10-21T14:52:27+00:00" + "time": "2025-11-26T12:16:36+00:00" }, { "name": "utopia-php/dsn", @@ -4158,16 +4214,16 @@ }, { "name": "utopia-php/emails", - "version": "0.6.2", + "version": "0.6.3", "source": { "type": "git", "url": "https://github.com/utopia-php/emails.git", - "reference": "9c4c40cf7c03c2e9e21364566f9b192d03ea93c9" + "reference": "9524d7f7bd1651a06fef8a3d964f774b04fe2918" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/emails/zipball/9c4c40cf7c03c2e9e21364566f9b192d03ea93c9", - "reference": "9c4c40cf7c03c2e9e21364566f9b192d03ea93c9", + "url": "https://api.github.com/repos/utopia-php/emails/zipball/9524d7f7bd1651a06fef8a3d964f774b04fe2918", + "reference": "9524d7f7bd1651a06fef8a3d964f774b04fe2918", "shasum": "" }, "require": { @@ -4175,7 +4231,7 @@ "utopia-php/cli": "^0.15", "utopia-php/domains": "^0.9", "utopia-php/fetch": "^0.4", - "utopia-php/validators": "^0.0.2" + "utopia-php/validators": "0.*" }, "require-dev": { "laravel/pint": "1.25.*", @@ -4212,9 +4268,9 @@ ], "support": { "issues": "https://github.com/utopia-php/emails/issues", - "source": "https://github.com/utopia-php/emails/tree/0.6.2" + "source": "https://github.com/utopia-php/emails/tree/0.6.3" }, - "time": "2025-10-28T16:08:17+00:00" + "time": "2025-11-26T12:27:47+00:00" }, { "name": "utopia-php/fetch", @@ -4257,22 +4313,23 @@ }, { "name": "utopia-php/framework", - "version": "0.33.30", + "version": "0.33.33", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "07cf699a7c47bd1a03b4da1812f1719a66b3c924" + "reference": "838e3a28276e73187bc34a314f014096dc92191b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/07cf699a7c47bd1a03b4da1812f1719a66b3c924", - "reference": "07cf699a7c47bd1a03b4da1812f1719a66b3c924", + "url": "https://api.github.com/repos/utopia-php/http/zipball/838e3a28276e73187bc34a314f014096dc92191b", + "reference": "838e3a28276e73187bc34a314f014096dc92191b", "shasum": "" }, "require": { "php": ">=8.1", "utopia-php/compression": "0.1.*", - "utopia-php/telemetry": "0.1.*" + "utopia-php/telemetry": "0.1.*", + "utopia-php/validators": "0.1.*" }, "require-dev": { "laravel/pint": "^1.2", @@ -4298,9 +4355,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.30" + "source": "https://github.com/utopia-php/http/tree/0.33.33" }, - "time": "2025-11-18T12:18:00+00:00" + "time": "2025-11-25T10:21:13+00:00" }, { "name": "utopia-php/image", @@ -4505,16 +4562,16 @@ }, { "name": "utopia-php/migration", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "18bd7d39dcee09280f40edb12879c727ecec98d3" + "reference": "f84a064b451ad688c313b6a0b6cc46664b449f0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/18bd7d39dcee09280f40edb12879c727ecec98d3", - "reference": "18bd7d39dcee09280f40edb12879c727ecec98d3", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/f84a064b451ad688c313b6a0b6cc46664b449f0b", + "reference": "f84a064b451ad688c313b6a0b6cc46664b449f0b", "shasum": "" }, "require": { @@ -4554,9 +4611,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.4.0" + "source": "https://github.com/utopia-php/migration/tree/1.4.1" }, - "time": "2025-11-21T06:08:59+00:00" + "time": "2025-11-25T10:26:11+00:00" }, { "name": "utopia-php/mongo", @@ -5159,26 +5216,25 @@ }, { "name": "utopia-php/validators", - "version": "0.0.2", + "version": "0.1.0", "source": { "type": "git", "url": "https://github.com/utopia-php/validators.git", - "reference": "894210695c5d35fa248fb65f7fe7237b6ff4fb0b" + "reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/validators/zipball/894210695c5d35fa248fb65f7fe7237b6ff4fb0b", - "reference": "894210695c5d35fa248fb65f7fe7237b6ff4fb0b", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/5c57d5b6cf964f8981807c1d3ea8df620c869080", + "reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.0" }, "require-dev": { - "ext-xdebug": "*", - "laravel/pint": "^1.2", + "laravel/pint": "1.*", "phpstan/phpstan": "1.*", - "phpunit/phpunit": "^9.5.25" + "phpunit/phpunit": "11.*" }, "type": "library", "autoload": { @@ -5199,22 +5255,22 @@ ], "support": { "issues": "https://github.com/utopia-php/validators/issues", - "source": "https://github.com/utopia-php/validators/tree/0.0.2" + "source": "https://github.com/utopia-php/validators/tree/0.1.0" }, - "time": "2025-10-20T21:52:28+00:00" + "time": "2025-11-18T11:05:46+00:00" }, { "name": "utopia-php/vcs", - "version": "0.12.0", + "version": "0.13.0", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "28457cf347972c4ec95d3ca77776a4921364a665" + "reference": "c59e21db5ca42014fe2071fec3c2f814efcc86dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/28457cf347972c4ec95d3ca77776a4921364a665", - "reference": "28457cf347972c4ec95d3ca77776a4921364a665", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/c59e21db5ca42014fe2071fec3c2f814efcc86dd", + "reference": "c59e21db5ca42014fe2071fec3c2f814efcc86dd", "shasum": "" }, "require": { @@ -5248,9 +5304,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/0.12.0" + "source": "https://github.com/utopia-php/vcs/tree/0.13.0" }, - "time": "2025-10-22T12:58:29+00:00" + "time": "2025-11-28T08:42:31+00:00" }, { "name": "utopia-php/websocket", @@ -5428,16 +5484,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.5.8", + "version": "1.5.9", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "05367bc4a4c3e020e9aca114ae875b626ce8fc55" + "reference": "ee434aa00a9185380b9a39bb46bf86d7104d3a93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/05367bc4a4c3e020e9aca114ae875b626ce8fc55", - "reference": "05367bc4a4c3e020e9aca114ae875b626ce8fc55", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/ee434aa00a9185380b9a39bb46bf86d7104d3a93", + "reference": "ee434aa00a9185380b9a39bb46bf86d7104d3a93", "shasum": "" }, "require": { @@ -5473,9 +5529,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.5.8" + "source": "https://github.com/appwrite/sdk-generator/tree/1.5.9" }, - "time": "2025-11-20T11:00:34+00:00" + "time": "2025-11-25T05:22:25+00:00" }, { "name": "doctrine/annotations", @@ -5703,16 +5759,16 @@ }, { "name": "laravel/pint", - "version": "v1.25.1", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", + "url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f", + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f", "shasum": "" }, "require": { @@ -5723,13 +5779,13 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.87.2", - "illuminate/view": "^11.46.0", - "larastan/larastan": "^3.7.1", - "laravel-zero/framework": "^11.45.0", + "friendsofphp/php-cs-fixer": "^3.90.0", + "illuminate/view": "^12.40.1", + "larastan/larastan": "^3.8.0", + "laravel-zero/framework": "^12.0.4", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.1", - "pestphp/pest": "^2.36.0" + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest": "^3.8.4" }, "bin": [ "builds/pint" @@ -5755,6 +5811,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -5765,7 +5822,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-09-19T02:57:12+00:00" + "time": "2025-11-25T21:15:52+00:00" }, { "name": "matthiasmullie/minify", @@ -6655,16 +6712,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.29", + "version": "9.6.30", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" + "reference": "b69489b312503bf8fa6d75a76916919d7d2fa6d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", - "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b69489b312503bf8fa6d75a76916919d7d2fa6d4", + "reference": "b69489b312503bf8fa6d75a76916919d7d2fa6d4", "shasum": "" }, "require": { @@ -6738,7 +6795,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.30" }, "funding": [ { @@ -6762,7 +6819,7 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:29:11+00:00" + "time": "2025-12-01T07:35:08+00:00" }, { "name": "psr/cache", @@ -7922,47 +7979,39 @@ }, { "name": "symfony/console", - "version": "v7.3.6", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" + "reference": "307d3cf852f5ead3618ac60ecbedbdd512c348b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "url": "https://api.github.com/repos/symfony/console/zipball/307d3cf852f5ead3618ac60ecbedbdd512c348b1", + "reference": "307d3cf852f5ead3618ac60ecbedbdd512c348b1", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" + "symfony/string": "^7.4|^8.0" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -7996,7 +8045,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.6" + "source": "https://github.com/symfony/console/tree/v8.0.0" }, "funding": [ { @@ -8016,29 +8065,29 @@ "type": "tidelift" } ], - "time": "2025-11-04T01:21:42+00:00" + "time": "2025-11-21T13:19:49+00:00" }, { "name": "symfony/filesystem", - "version": "v7.3.6", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" + "reference": "7fc96ae83372620eaba3826874f46e26295768ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7fc96ae83372620eaba3826874f46e26295768ca", + "reference": "7fc96ae83372620eaba3826874f46e26295768ca", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -8066,7 +8115,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.6" + "source": "https://github.com/symfony/filesystem/tree/v8.0.0" }, "funding": [ { @@ -8086,27 +8135,27 @@ "type": "tidelift" } ], - "time": "2025-11-05T09:52:27+00:00" + "time": "2025-11-05T14:36:47+00:00" }, { "name": "symfony/finder", - "version": "v7.3.5", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "url": "https://api.github.com/repos/symfony/finder/zipball/7598dd5770580fa3517ec83e8da0c9b9e01f4291", + "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -8134,7 +8183,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.5" + "source": "https://github.com/symfony/finder/tree/v8.0.0" }, "funding": [ { @@ -8154,24 +8203,24 @@ "type": "tidelift" } ], - "time": "2025-10-15T18:45:57+00:00" + "time": "2025-11-05T14:36:47+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -8205,7 +8254,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" }, "funding": [ { @@ -8225,7 +8274,7 @@ "type": "tidelift" } ], - "time": "2025-08-05T10:16:07+00:00" + "time": "2025-11-12T15:55:31+00:00" }, { "name": "symfony/polyfill-ctype", @@ -8559,20 +8608,20 @@ }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/a0a750500c4ce900d69ba4e9faf16f82c10ee149", + "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -8600,7 +8649,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.4" + "source": "https://github.com/symfony/process/tree/v8.0.0" }, "funding": [ { @@ -8620,38 +8669,38 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-10-16T16:25:44+00:00" }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "f929eccf09531078c243df72398560e32fa4cf4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/f929eccf09531078c243df72398560e32fa4cf4f", + "reference": "f929eccf09531078c243df72398560e32fa4cf4f", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -8690,7 +8739,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v8.0.0" }, "funding": [ { @@ -8710,7 +8759,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-09-11T14:37:55+00:00" }, { "name": "textalk/websocket", diff --git a/docker-compose.yml b/docker-compose.yml index 24f555eadc..ff797d33b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -304,6 +304,7 @@ services: - _APP_DB_PASS_VECTORDB - _APP_USAGE_STATS - _APP_LOGGING_CONFIG + - _APP_LOGGING_CONFIG_REALTIME - _APP_DATABASE_SHARED_TABLES appwrite-worker-audits: @@ -1019,7 +1020,7 @@ services: appwrite-assistant: container_name: appwrite-assistant - image: appwrite/assistant:0.8.3 + image: appwrite/assistant:0.8.4 networks: - appwrite environment: @@ -1027,7 +1028,7 @@ services: appwrite-browser: container_name: appwrite-browser - image: appwrite/browser:0.3.1 + image: appwrite/browser:0.3.2 networks: - appwrite diff --git a/docs/examples/1.5.x/client-rest/examples/account/create-challenge.md b/docs/examples/1.5.x/client-rest/examples/account/create-challenge.md index dbdbc1f16a..8d803e56a9 100644 --- a/docs/examples/1.5.x/client-rest/examples/account/create-challenge.md +++ b/docs/examples/1.5.x/client-rest/examples/account/create-challenge.md @@ -1,4 +1,4 @@ -POST /v1/account/mfa/challenge HTTP/1.1 +POST /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.5.0 diff --git a/docs/examples/1.5.x/client-rest/examples/account/create-mfa-challenge.md b/docs/examples/1.5.x/client-rest/examples/account/create-mfa-challenge.md index 2019a1e52c..0553c4b5ba 100644 --- a/docs/examples/1.5.x/client-rest/examples/account/create-mfa-challenge.md +++ b/docs/examples/1.5.x/client-rest/examples/account/create-mfa-challenge.md @@ -1,4 +1,4 @@ -POST /v1/account/mfa/challenge HTTP/1.1 +POST /v1/account/mfa/challenges HTTP/1.1 Host: <REGION>.cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.6.0 diff --git a/docs/examples/1.5.x/client-rest/examples/account/create2f-a-challenge.md b/docs/examples/1.5.x/client-rest/examples/account/create2f-a-challenge.md index dbdbc1f16a..8d803e56a9 100644 --- a/docs/examples/1.5.x/client-rest/examples/account/create2f-a-challenge.md +++ b/docs/examples/1.5.x/client-rest/examples/account/create2f-a-challenge.md @@ -1,4 +1,4 @@ -POST /v1/account/mfa/challenge HTTP/1.1 +POST /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.5.0 diff --git a/docs/examples/1.5.x/client-rest/examples/account/update-challenge.md b/docs/examples/1.5.x/client-rest/examples/account/update-challenge.md index 5e50a0e88d..00c85da373 100644 --- a/docs/examples/1.5.x/client-rest/examples/account/update-challenge.md +++ b/docs/examples/1.5.x/client-rest/examples/account/update-challenge.md @@ -1,4 +1,4 @@ -PUT /v1/account/mfa/challenge HTTP/1.1 +PUT /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.5.0 diff --git a/docs/examples/1.5.x/client-rest/examples/account/update-mfa-challenge.md b/docs/examples/1.5.x/client-rest/examples/account/update-mfa-challenge.md index 520b587562..093cc37930 100644 --- a/docs/examples/1.5.x/client-rest/examples/account/update-mfa-challenge.md +++ b/docs/examples/1.5.x/client-rest/examples/account/update-mfa-challenge.md @@ -1,4 +1,4 @@ -PUT /v1/account/mfa/challenge HTTP/1.1 +PUT /v1/account/mfa/challenges HTTP/1.1 Host: <REGION>.cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.6.0 diff --git a/docs/examples/1.5.x/server-rest/examples/account/create-challenge.md b/docs/examples/1.5.x/server-rest/examples/account/create-challenge.md index dbdbc1f16a..8d803e56a9 100644 --- a/docs/examples/1.5.x/server-rest/examples/account/create-challenge.md +++ b/docs/examples/1.5.x/server-rest/examples/account/create-challenge.md @@ -1,4 +1,4 @@ -POST /v1/account/mfa/challenge HTTP/1.1 +POST /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.5.0 diff --git a/docs/examples/1.5.x/server-rest/examples/account/create-mfa-challenge.md b/docs/examples/1.5.x/server-rest/examples/account/create-mfa-challenge.md index 2019a1e52c..0553c4b5ba 100644 --- a/docs/examples/1.5.x/server-rest/examples/account/create-mfa-challenge.md +++ b/docs/examples/1.5.x/server-rest/examples/account/create-mfa-challenge.md @@ -1,4 +1,4 @@ -POST /v1/account/mfa/challenge HTTP/1.1 +POST /v1/account/mfa/challenges HTTP/1.1 Host: <REGION>.cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.6.0 diff --git a/docs/examples/1.5.x/server-rest/examples/account/create2f-a-challenge.md b/docs/examples/1.5.x/server-rest/examples/account/create2f-a-challenge.md index dbdbc1f16a..8d803e56a9 100644 --- a/docs/examples/1.5.x/server-rest/examples/account/create2f-a-challenge.md +++ b/docs/examples/1.5.x/server-rest/examples/account/create2f-a-challenge.md @@ -1,4 +1,4 @@ -POST /v1/account/mfa/challenge HTTP/1.1 +POST /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.5.0 diff --git a/docs/examples/1.5.x/server-rest/examples/account/update-challenge.md b/docs/examples/1.5.x/server-rest/examples/account/update-challenge.md index 5e50a0e88d..00c85da373 100644 --- a/docs/examples/1.5.x/server-rest/examples/account/update-challenge.md +++ b/docs/examples/1.5.x/server-rest/examples/account/update-challenge.md @@ -1,4 +1,4 @@ -PUT /v1/account/mfa/challenge HTTP/1.1 +PUT /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.5.0 diff --git a/docs/examples/1.5.x/server-rest/examples/account/update-mfa-challenge.md b/docs/examples/1.5.x/server-rest/examples/account/update-mfa-challenge.md index 520b587562..093cc37930 100644 --- a/docs/examples/1.5.x/server-rest/examples/account/update-mfa-challenge.md +++ b/docs/examples/1.5.x/server-rest/examples/account/update-mfa-challenge.md @@ -1,4 +1,4 @@ -PUT /v1/account/mfa/challenge HTTP/1.1 +PUT /v1/account/mfa/challenges HTTP/1.1 Host: <REGION>.cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.6.0 diff --git a/docs/examples/1.6.x/client-rest/examples/account/create-mfa-challenge.md b/docs/examples/1.6.x/client-rest/examples/account/create-mfa-challenge.md index 95bf2c4926..c3007fc290 100644 --- a/docs/examples/1.6.x/client-rest/examples/account/create-mfa-challenge.md +++ b/docs/examples/1.6.x/client-rest/examples/account/create-mfa-challenge.md @@ -1,4 +1,4 @@ -POST /v1/account/mfa/challenge HTTP/1.1 +POST /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.6.0 diff --git a/docs/examples/1.6.x/client-rest/examples/account/update-mfa-challenge.md b/docs/examples/1.6.x/client-rest/examples/account/update-mfa-challenge.md index 5bd401cc4e..779aeb2ecd 100644 --- a/docs/examples/1.6.x/client-rest/examples/account/update-mfa-challenge.md +++ b/docs/examples/1.6.x/client-rest/examples/account/update-mfa-challenge.md @@ -1,4 +1,4 @@ -PUT /v1/account/mfa/challenge HTTP/1.1 +PUT /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.6.0 diff --git a/docs/examples/1.6.x/server-rest/examples/account/create-mfa-challenge.md b/docs/examples/1.6.x/server-rest/examples/account/create-mfa-challenge.md index 95bf2c4926..c3007fc290 100644 --- a/docs/examples/1.6.x/server-rest/examples/account/create-mfa-challenge.md +++ b/docs/examples/1.6.x/server-rest/examples/account/create-mfa-challenge.md @@ -1,4 +1,4 @@ -POST /v1/account/mfa/challenge HTTP/1.1 +POST /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.6.0 diff --git a/docs/examples/1.6.x/server-rest/examples/account/update-mfa-challenge.md b/docs/examples/1.6.x/server-rest/examples/account/update-mfa-challenge.md index 5bd401cc4e..779aeb2ecd 100644 --- a/docs/examples/1.6.x/server-rest/examples/account/update-mfa-challenge.md +++ b/docs/examples/1.6.x/server-rest/examples/account/update-mfa-challenge.md @@ -1,4 +1,4 @@ -PUT /v1/account/mfa/challenge HTTP/1.1 +PUT /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.6.0 diff --git a/docs/examples/1.7.x/client-rest/examples/account/create-mfa-challenge.md b/docs/examples/1.7.x/client-rest/examples/account/create-mfa-challenge.md index 9a84c0ef69..bda2de889d 100644 --- a/docs/examples/1.7.x/client-rest/examples/account/create-mfa-challenge.md +++ b/docs/examples/1.7.x/client-rest/examples/account/create-mfa-challenge.md @@ -1,4 +1,4 @@ -POST /v1/account/mfa/challenge HTTP/1.1 +POST /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.7.0 diff --git a/docs/examples/1.7.x/client-rest/examples/account/update-mfa-challenge.md b/docs/examples/1.7.x/client-rest/examples/account/update-mfa-challenge.md index ddc27ae334..506059dc3d 100644 --- a/docs/examples/1.7.x/client-rest/examples/account/update-mfa-challenge.md +++ b/docs/examples/1.7.x/client-rest/examples/account/update-mfa-challenge.md @@ -1,4 +1,4 @@ -PUT /v1/account/mfa/challenge HTTP/1.1 +PUT /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.7.0 diff --git a/docs/examples/1.7.x/server-rest/examples/account/create-mfa-challenge.md b/docs/examples/1.7.x/server-rest/examples/account/create-mfa-challenge.md index 9a84c0ef69..bda2de889d 100644 --- a/docs/examples/1.7.x/server-rest/examples/account/create-mfa-challenge.md +++ b/docs/examples/1.7.x/server-rest/examples/account/create-mfa-challenge.md @@ -1,4 +1,4 @@ -POST /v1/account/mfa/challenge HTTP/1.1 +POST /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.7.0 diff --git a/docs/examples/1.7.x/server-rest/examples/account/update-mfa-challenge.md b/docs/examples/1.7.x/server-rest/examples/account/update-mfa-challenge.md index ddc27ae334..506059dc3d 100644 --- a/docs/examples/1.7.x/server-rest/examples/account/update-mfa-challenge.md +++ b/docs/examples/1.7.x/server-rest/examples/account/update-mfa-challenge.md @@ -1,4 +1,4 @@ -PUT /v1/account/mfa/challenge HTTP/1.1 +PUT /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.7.0 diff --git a/docs/examples/1.8.x/client-android/java/account/create-o-auth-2-session.md b/docs/examples/1.8.x/client-android/java/account/create-o-auth-2-session.md index 4420859ce3..5ada9368ea 100644 --- a/docs/examples/1.8.x/client-android/java/account/create-o-auth-2-session.md +++ b/docs/examples/1.8.x/client-android/java/account/create-o-auth-2-session.md @@ -13,7 +13,7 @@ account.createOAuth2Session( OAuthProvider.AMAZON, // provider "https://example.com", // success (optional) "https://example.com", // failure (optional) - listOf(), // scopes (optional) + List.of(), // scopes (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/client-android/java/account/create-o-auth-2-token.md b/docs/examples/1.8.x/client-android/java/account/create-o-auth-2-token.md index e5590c8ceb..f1122dc8f3 100644 --- a/docs/examples/1.8.x/client-android/java/account/create-o-auth-2-token.md +++ b/docs/examples/1.8.x/client-android/java/account/create-o-auth-2-token.md @@ -13,7 +13,7 @@ account.createOAuth2Token( OAuthProvider.AMAZON, // provider "https://example.com", // success (optional) "https://example.com", // failure (optional) - listOf(), // scopes (optional) + List.of(), // scopes (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/client-android/java/account/list-identities.md b/docs/examples/1.8.x/client-android/java/account/list-identities.md index 327fe39537..e5dd44d9b7 100644 --- a/docs/examples/1.8.x/client-android/java/account/list-identities.md +++ b/docs/examples/1.8.x/client-android/java/account/list-identities.md @@ -9,7 +9,7 @@ Client client = new Client(context) Account account = new Account(client); account.listIdentities( - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/client-android/java/account/list-logs.md b/docs/examples/1.8.x/client-android/java/account/list-logs.md index 4562ecc3ba..8b6ec85067 100644 --- a/docs/examples/1.8.x/client-android/java/account/list-logs.md +++ b/docs/examples/1.8.x/client-android/java/account/list-logs.md @@ -9,7 +9,7 @@ Client client = new Client(context) Account account = new Account(client); account.listLogs( - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/client-android/java/account/update-prefs.md b/docs/examples/1.8.x/client-android/java/account/update-prefs.md index 4bd6940c61..2682fe3aeb 100644 --- a/docs/examples/1.8.x/client-android/java/account/update-prefs.md +++ b/docs/examples/1.8.x/client-android/java/account/update-prefs.md @@ -9,10 +9,10 @@ Client client = new Client(context) Account account = new Account(client); account.updatePrefs( - mapOf( - "language" to "en", - "timezone" to "UTC", - "darkTheme" to true + Map.of( + "language", "en", + "timezone", "UTC", + "darkTheme", true ), // prefs new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/client-android/java/avatars/get-screenshot.md b/docs/examples/1.8.x/client-android/java/avatars/get-screenshot.md index 48babdca20..0f6e41451a 100644 --- a/docs/examples/1.8.x/client-android/java/avatars/get-screenshot.md +++ b/docs/examples/1.8.x/client-android/java/avatars/get-screenshot.md @@ -13,7 +13,7 @@ Avatars avatars = new Avatars(client); avatars.getScreenshot( "https://example.com", // url - mapOf( "a" to "b" ), // headers (optional) + Map.of("a", "b"), // headers (optional) 1, // viewportWidth (optional) 1, // viewportHeight (optional) 0.1, // scale (optional) @@ -26,7 +26,7 @@ avatars.getScreenshot( -180, // longitude (optional) 0, // accuracy (optional) false, // touch (optional) - listOf(), // permissions (optional) + List.of(), // permissions (optional) 0, // sleep (optional) 0, // width (optional) 0, // height (optional) diff --git a/docs/examples/1.8.x/client-android/java/databases/create-document.md b/docs/examples/1.8.x/client-android/java/databases/create-document.md index bfd4601ab4..d95db8a9b0 100644 --- a/docs/examples/1.8.x/client-android/java/databases/create-document.md +++ b/docs/examples/1.8.x/client-android/java/databases/create-document.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.Databases; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Databases; Client client = new Client(context) .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -14,14 +14,14 @@ databases.createDocument( "", // databaseId "", // collectionId "", // documentId - mapOf( - "username" to "walter.obrien", - "email" to "walter.obrien@example.com", - "fullName" to "Walter O'Brien", - "age" to 30, - "isAdmin" to false + Map.of( + "username", "walter.obrien", + "email", "walter.obrien@example.com", + "fullName", "Walter O'Brien", + "age", 30, + "isAdmin", false ), // data - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/client-android/java/databases/create-operations.md b/docs/examples/1.8.x/client-android/java/databases/create-operations.md index def58af773..a8635d81e9 100644 --- a/docs/examples/1.8.x/client-android/java/databases/create-operations.md +++ b/docs/examples/1.8.x/client-android/java/databases/create-operations.md @@ -10,17 +10,15 @@ Databases databases = new Databases(client); databases.createOperations( "", // transactionId - listOf( - { - "action": "create", - "databaseId": "", - "collectionId": "", - "documentId": "", - "data": { - "name": "Walter O'Brien" - } - } - ), // operations (optional) + List.of(Map.of( + "action", "create", + "databaseId", "", + "collectionId", "", + "documentId", "", + "data", Map.of( + "name", "Walter O'Brien" + ) + )), // operations (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/client-android/java/databases/get-document.md b/docs/examples/1.8.x/client-android/java/databases/get-document.md index 92d6b5ed23..85f9bb9b13 100644 --- a/docs/examples/1.8.x/client-android/java/databases/get-document.md +++ b/docs/examples/1.8.x/client-android/java/databases/get-document.md @@ -12,7 +12,7 @@ databases.getDocument( "", // databaseId "", // collectionId "", // documentId - listOf(), // queries (optional) + List.of(), // queries (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/client-android/java/databases/list-documents.md b/docs/examples/1.8.x/client-android/java/databases/list-documents.md index e4102ec542..5440f786cc 100644 --- a/docs/examples/1.8.x/client-android/java/databases/list-documents.md +++ b/docs/examples/1.8.x/client-android/java/databases/list-documents.md @@ -11,7 +11,7 @@ Databases databases = new Databases(client); databases.listDocuments( "", // databaseId "", // collectionId - listOf(), // queries (optional) + List.of(), // queries (optional) "", // transactionId (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/client-android/java/databases/list-transactions.md b/docs/examples/1.8.x/client-android/java/databases/list-transactions.md index 39f58f3490..899a3ea181 100644 --- a/docs/examples/1.8.x/client-android/java/databases/list-transactions.md +++ b/docs/examples/1.8.x/client-android/java/databases/list-transactions.md @@ -9,7 +9,7 @@ Client client = new Client(context) Databases databases = new Databases(client); databases.listTransactions( - listOf(), // queries (optional) + List.of(), // queries (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/client-android/java/databases/update-document.md b/docs/examples/1.8.x/client-android/java/databases/update-document.md index d3a3967d5b..0f13f74e08 100644 --- a/docs/examples/1.8.x/client-android/java/databases/update-document.md +++ b/docs/examples/1.8.x/client-android/java/databases/update-document.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.Databases; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Databases; Client client = new Client(context) .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -14,8 +14,8 @@ databases.updateDocument( "", // databaseId "", // collectionId "", // documentId - mapOf( "a" to "b" ), // data (optional) - listOf(Permission.read(Role.any())), // permissions (optional) + Map.of("a", "b"), // data (optional) + List.of(Permission.read(Role.any())), // permissions (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/client-android/java/databases/upsert-document.md b/docs/examples/1.8.x/client-android/java/databases/upsert-document.md index e46afa10a9..8c72734f98 100644 --- a/docs/examples/1.8.x/client-android/java/databases/upsert-document.md +++ b/docs/examples/1.8.x/client-android/java/databases/upsert-document.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.Databases; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Databases; Client client = new Client(context) .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -14,8 +14,8 @@ databases.upsertDocument( "", // databaseId "", // collectionId "", // documentId - mapOf( "a" to "b" ), // data - listOf(Permission.read(Role.any())), // permissions (optional) + Map.of("a", "b"), // data + List.of(Permission.read(Role.any())), // permissions (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/client-android/java/functions/create-execution.md b/docs/examples/1.8.x/client-android/java/functions/create-execution.md index 758881d13c..4aea0929f1 100644 --- a/docs/examples/1.8.x/client-android/java/functions/create-execution.md +++ b/docs/examples/1.8.x/client-android/java/functions/create-execution.md @@ -15,7 +15,7 @@ functions.createExecution( false, // async (optional) "", // path (optional) ExecutionMethod.GET, // method (optional) - mapOf( "a" to "b" ), // headers (optional) + Map.of("a", "b"), // headers (optional) "", // scheduledAt (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/client-android/java/functions/list-executions.md b/docs/examples/1.8.x/client-android/java/functions/list-executions.md index c9a1df107e..893d098998 100644 --- a/docs/examples/1.8.x/client-android/java/functions/list-executions.md +++ b/docs/examples/1.8.x/client-android/java/functions/list-executions.md @@ -10,7 +10,7 @@ Functions functions = new Functions(client); functions.listExecutions( "", // functionId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/client-android/java/graphql/mutation.md b/docs/examples/1.8.x/client-android/java/graphql/mutation.md index 25f095e1b1..445195275a 100644 --- a/docs/examples/1.8.x/client-android/java/graphql/mutation.md +++ b/docs/examples/1.8.x/client-android/java/graphql/mutation.md @@ -9,7 +9,7 @@ Client client = new Client(context) Graphql graphql = new Graphql(client); graphql.mutation( - mapOf( "a" to "b" ), // query + Map.of("a", "b"), // query new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/client-android/java/graphql/query.md b/docs/examples/1.8.x/client-android/java/graphql/query.md index 6b2a04d0b6..b39f128165 100644 --- a/docs/examples/1.8.x/client-android/java/graphql/query.md +++ b/docs/examples/1.8.x/client-android/java/graphql/query.md @@ -9,7 +9,7 @@ Client client = new Client(context) Graphql graphql = new Graphql(client); graphql.query( - mapOf( "a" to "b" ), // query + Map.of("a", "b"), // query new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/client-android/java/storage/create-file.md b/docs/examples/1.8.x/client-android/java/storage/create-file.md index 8de00099b0..518bbc9d29 100644 --- a/docs/examples/1.8.x/client-android/java/storage/create-file.md +++ b/docs/examples/1.8.x/client-android/java/storage/create-file.md @@ -1,9 +1,9 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; import io.appwrite.models.InputFile; -import io.appwrite.services.Storage; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Storage; Client client = new Client(context) .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,7 +15,7 @@ storage.createFile( "", // bucketId "", // fileId InputFile.fromPath("file.png"), // file - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/client-android/java/storage/list-files.md b/docs/examples/1.8.x/client-android/java/storage/list-files.md index 178027cc5d..05292a6465 100644 --- a/docs/examples/1.8.x/client-android/java/storage/list-files.md +++ b/docs/examples/1.8.x/client-android/java/storage/list-files.md @@ -10,7 +10,7 @@ Storage storage = new Storage(client); storage.listFiles( "", // bucketId - listOf(), // queries (optional) + List.of(), // queries (optional) "", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/client-android/java/storage/update-file.md b/docs/examples/1.8.x/client-android/java/storage/update-file.md index 1e21b3fae1..40cc61b1bf 100644 --- a/docs/examples/1.8.x/client-android/java/storage/update-file.md +++ b/docs/examples/1.8.x/client-android/java/storage/update-file.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.Storage; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Storage; Client client = new Client(context) .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -14,7 +14,7 @@ storage.updateFile( "", // bucketId "", // fileId "", // name (optional) - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/client-android/java/tablesdb/create-operations.md b/docs/examples/1.8.x/client-android/java/tablesdb/create-operations.md index 7ca288c2ae..0e8cb4b067 100644 --- a/docs/examples/1.8.x/client-android/java/tablesdb/create-operations.md +++ b/docs/examples/1.8.x/client-android/java/tablesdb/create-operations.md @@ -10,17 +10,15 @@ TablesDB tablesDB = new TablesDB(client); tablesDB.createOperations( "", // transactionId - listOf( - { - "action": "create", - "databaseId": "", - "tableId": "", - "rowId": "", - "data": { - "name": "Walter O'Brien" - } - } - ), // operations (optional) + List.of(Map.of( + "action", "create", + "databaseId", "", + "tableId", "", + "rowId", "", + "data", Map.of( + "name", "Walter O'Brien" + ) + )), // operations (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/client-android/java/tablesdb/create-row.md b/docs/examples/1.8.x/client-android/java/tablesdb/create-row.md index f7aa10e5c7..b3e4f310db 100644 --- a/docs/examples/1.8.x/client-android/java/tablesdb/create-row.md +++ b/docs/examples/1.8.x/client-android/java/tablesdb/create-row.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.TablesDB; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.TablesDB; Client client = new Client(context) .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -14,14 +14,14 @@ tablesDB.createRow( "", // databaseId "", // tableId "", // rowId - mapOf( - "username" to "walter.obrien", - "email" to "walter.obrien@example.com", - "fullName" to "Walter O'Brien", - "age" to 30, - "isAdmin" to false + Map.of( + "username", "walter.obrien", + "email", "walter.obrien@example.com", + "fullName", "Walter O'Brien", + "age", 30, + "isAdmin", false ), // data - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/client-android/java/tablesdb/get-row.md b/docs/examples/1.8.x/client-android/java/tablesdb/get-row.md index 45efc6b061..2bf847284a 100644 --- a/docs/examples/1.8.x/client-android/java/tablesdb/get-row.md +++ b/docs/examples/1.8.x/client-android/java/tablesdb/get-row.md @@ -12,7 +12,7 @@ tablesDB.getRow( "", // databaseId "", // tableId "", // rowId - listOf(), // queries (optional) + List.of(), // queries (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/client-android/java/tablesdb/list-rows.md b/docs/examples/1.8.x/client-android/java/tablesdb/list-rows.md index 3bd1e1c77a..0833929eb1 100644 --- a/docs/examples/1.8.x/client-android/java/tablesdb/list-rows.md +++ b/docs/examples/1.8.x/client-android/java/tablesdb/list-rows.md @@ -11,7 +11,7 @@ TablesDB tablesDB = new TablesDB(client); tablesDB.listRows( "", // databaseId "", // tableId - listOf(), // queries (optional) + List.of(), // queries (optional) "", // transactionId (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/client-android/java/tablesdb/list-transactions.md b/docs/examples/1.8.x/client-android/java/tablesdb/list-transactions.md index c37ccf1931..ce0147b036 100644 --- a/docs/examples/1.8.x/client-android/java/tablesdb/list-transactions.md +++ b/docs/examples/1.8.x/client-android/java/tablesdb/list-transactions.md @@ -9,7 +9,7 @@ Client client = new Client(context) TablesDB tablesDB = new TablesDB(client); tablesDB.listTransactions( - listOf(), // queries (optional) + List.of(), // queries (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/client-android/java/tablesdb/update-row.md b/docs/examples/1.8.x/client-android/java/tablesdb/update-row.md index 3abaf90603..89f2646d9d 100644 --- a/docs/examples/1.8.x/client-android/java/tablesdb/update-row.md +++ b/docs/examples/1.8.x/client-android/java/tablesdb/update-row.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.TablesDB; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.TablesDB; Client client = new Client(context) .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -14,8 +14,8 @@ tablesDB.updateRow( "", // databaseId "", // tableId "", // rowId - mapOf( "a" to "b" ), // data (optional) - listOf(Permission.read(Role.any())), // permissions (optional) + Map.of("a", "b"), // data (optional) + List.of(Permission.read(Role.any())), // permissions (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/client-android/java/tablesdb/upsert-row.md b/docs/examples/1.8.x/client-android/java/tablesdb/upsert-row.md index 6f979fc126..c6cfe6d1bc 100644 --- a/docs/examples/1.8.x/client-android/java/tablesdb/upsert-row.md +++ b/docs/examples/1.8.x/client-android/java/tablesdb/upsert-row.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.TablesDB; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.TablesDB; Client client = new Client(context) .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -14,8 +14,8 @@ tablesDB.upsertRow( "", // databaseId "", // tableId "", // rowId - mapOf( "a" to "b" ), // data (optional) - listOf(Permission.read(Role.any())), // permissions (optional) + Map.of("a", "b"), // data (optional) + List.of(Permission.read(Role.any())), // permissions (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/client-android/java/teams/create-membership.md b/docs/examples/1.8.x/client-android/java/teams/create-membership.md index bb5293ef63..e5eee207dc 100644 --- a/docs/examples/1.8.x/client-android/java/teams/create-membership.md +++ b/docs/examples/1.8.x/client-android/java/teams/create-membership.md @@ -10,7 +10,7 @@ Teams teams = new Teams(client); teams.createMembership( "", // teamId - listOf(), // roles + List.of(), // roles "email@example.com", // email (optional) "", // userId (optional) "+12065550100", // phone (optional) diff --git a/docs/examples/1.8.x/client-android/java/teams/create.md b/docs/examples/1.8.x/client-android/java/teams/create.md index ae2fdf32c8..232d3b38ee 100644 --- a/docs/examples/1.8.x/client-android/java/teams/create.md +++ b/docs/examples/1.8.x/client-android/java/teams/create.md @@ -11,7 +11,7 @@ Teams teams = new Teams(client); teams.create( "", // teamId "", // name - listOf(), // roles (optional) + List.of(), // roles (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/client-android/java/teams/list-memberships.md b/docs/examples/1.8.x/client-android/java/teams/list-memberships.md index ae5cc69b4d..c8d3b610dc 100644 --- a/docs/examples/1.8.x/client-android/java/teams/list-memberships.md +++ b/docs/examples/1.8.x/client-android/java/teams/list-memberships.md @@ -10,7 +10,7 @@ Teams teams = new Teams(client); teams.listMemberships( "", // teamId - listOf(), // queries (optional) + List.of(), // queries (optional) "", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/client-android/java/teams/list.md b/docs/examples/1.8.x/client-android/java/teams/list.md index fff14e2992..1db3e67a2e 100644 --- a/docs/examples/1.8.x/client-android/java/teams/list.md +++ b/docs/examples/1.8.x/client-android/java/teams/list.md @@ -9,7 +9,7 @@ Client client = new Client(context) Teams teams = new Teams(client); teams.list( - listOf(), // queries (optional) + List.of(), // queries (optional) "", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/client-android/java/teams/update-membership.md b/docs/examples/1.8.x/client-android/java/teams/update-membership.md index 481be43107..f8adcf1fb8 100644 --- a/docs/examples/1.8.x/client-android/java/teams/update-membership.md +++ b/docs/examples/1.8.x/client-android/java/teams/update-membership.md @@ -11,7 +11,7 @@ Teams teams = new Teams(client); teams.updateMembership( "", // teamId "", // membershipId - listOf(), // roles + List.of(), // roles new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/client-android/java/teams/update-prefs.md b/docs/examples/1.8.x/client-android/java/teams/update-prefs.md index 5a0186ff31..9ea1487bd4 100644 --- a/docs/examples/1.8.x/client-android/java/teams/update-prefs.md +++ b/docs/examples/1.8.x/client-android/java/teams/update-prefs.md @@ -10,7 +10,7 @@ Teams teams = new Teams(client); teams.updatePrefs( "", // teamId - mapOf( "a" to "b" ), // prefs + Map.of("a", "b"), // prefs new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/client-android/kotlin/databases/create-operations.md b/docs/examples/1.8.x/client-android/kotlin/databases/create-operations.md index 62c93351e7..f3a419448b 100644 --- a/docs/examples/1.8.x/client-android/kotlin/databases/create-operations.md +++ b/docs/examples/1.8.x/client-android/kotlin/databases/create-operations.md @@ -10,15 +10,13 @@ val databases = Databases(client) val result = databases.createOperations( transactionId = "", - operations = listOf( - { - "action": "create", - "databaseId": "", - "collectionId": "", - "documentId": "", - "data": { - "name": "Walter O'Brien" - } - } - ), // (optional) + operations = listOf(mapOf( + "action" to "create", + "databaseId" to "", + "collectionId" to "", + "documentId" to "", + "data" to mapOf( + "name" to "Walter O'Brien" + ) + )), // (optional) ) \ No newline at end of file diff --git a/docs/examples/1.8.x/client-android/kotlin/tablesdb/create-operations.md b/docs/examples/1.8.x/client-android/kotlin/tablesdb/create-operations.md index 3073a00bca..7807102f27 100644 --- a/docs/examples/1.8.x/client-android/kotlin/tablesdb/create-operations.md +++ b/docs/examples/1.8.x/client-android/kotlin/tablesdb/create-operations.md @@ -10,15 +10,13 @@ val tablesDB = TablesDB(client) val result = tablesDB.createOperations( transactionId = "", - operations = listOf( - { - "action": "create", - "databaseId": "", - "tableId": "", - "rowId": "", - "data": { - "name": "Walter O'Brien" - } - } - ), // (optional) + operations = listOf(mapOf( + "action" to "create", + "databaseId" to "", + "tableId" to "", + "rowId" to "", + "data" to mapOf( + "name" to "Walter O'Brien" + ) + )), // (optional) ) \ No newline at end of file diff --git a/docs/examples/1.8.x/client-rest/examples/account/create-mfa-challenge.md b/docs/examples/1.8.x/client-rest/examples/account/create-mfa-challenge.md index dd5ef4c731..e5a5b0ea05 100644 --- a/docs/examples/1.8.x/client-rest/examples/account/create-mfa-challenge.md +++ b/docs/examples/1.8.x/client-rest/examples/account/create-mfa-challenge.md @@ -1,4 +1,4 @@ -POST /v1/account/mfa/challenge HTTP/1.1 +POST /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.8.0 diff --git a/docs/examples/1.8.x/client-rest/examples/account/update-mfa-challenge.md b/docs/examples/1.8.x/client-rest/examples/account/update-mfa-challenge.md index b6a7e92b28..df2cd9a1e8 100644 --- a/docs/examples/1.8.x/client-rest/examples/account/update-mfa-challenge.md +++ b/docs/examples/1.8.x/client-rest/examples/account/update-mfa-challenge.md @@ -1,4 +1,4 @@ -PUT /v1/account/mfa/challenge HTTP/1.1 +PUT /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.8.0 diff --git a/docs/examples/1.8.x/console-web/examples/functions/create-template-deployment.md b/docs/examples/1.8.x/console-web/examples/functions/create-template-deployment.md index 5bf812c1bc..414a0d0bfb 100644 --- a/docs/examples/1.8.x/console-web/examples/functions/create-template-deployment.md +++ b/docs/examples/1.8.x/console-web/examples/functions/create-template-deployment.md @@ -1,4 +1,4 @@ -import { Client, Functions, } from "@appwrite.io/console"; +import { Client, Functions, TemplateReferenceType } from "@appwrite.io/console"; const client = new Client() .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint @@ -11,7 +11,7 @@ const result = await functions.createTemplateDeployment({ repository: '', owner: '', rootDirectory: '', - type: .Commit, + type: TemplateReferenceType.Commit, reference: '', activate: false // optional }); diff --git a/docs/examples/1.8.x/console-web/examples/functions/create-vcs-deployment.md b/docs/examples/1.8.x/console-web/examples/functions/create-vcs-deployment.md index 33da9bfd70..d6f4e765e3 100644 --- a/docs/examples/1.8.x/console-web/examples/functions/create-vcs-deployment.md +++ b/docs/examples/1.8.x/console-web/examples/functions/create-vcs-deployment.md @@ -1,4 +1,4 @@ -import { Client, Functions, VCSDeploymentType } from "@appwrite.io/console"; +import { Client, Functions, VCSReferenceType } from "@appwrite.io/console"; const client = new Client() .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint @@ -8,7 +8,7 @@ const functions = new Functions(client); const result = await functions.createVcsDeployment({ functionId: '', - type: VCSDeploymentType.Branch, + type: VCSReferenceType.Branch, reference: '', activate: false // optional }); diff --git a/docs/examples/1.8.x/console-web/examples/sites/create-template-deployment.md b/docs/examples/1.8.x/console-web/examples/sites/create-template-deployment.md index 4f1d0184f7..1bfaeb6a7e 100644 --- a/docs/examples/1.8.x/console-web/examples/sites/create-template-deployment.md +++ b/docs/examples/1.8.x/console-web/examples/sites/create-template-deployment.md @@ -1,4 +1,4 @@ -import { Client, Sites, } from "@appwrite.io/console"; +import { Client, Sites, TemplateReferenceType } from "@appwrite.io/console"; const client = new Client() .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint @@ -11,7 +11,7 @@ const result = await sites.createTemplateDeployment({ repository: '', owner: '', rootDirectory: '', - type: .Branch, + type: TemplateReferenceType.Branch, reference: '', activate: false // optional }); diff --git a/docs/examples/1.8.x/console-web/examples/sites/create-vcs-deployment.md b/docs/examples/1.8.x/console-web/examples/sites/create-vcs-deployment.md index cc1fd1d301..80d9403ccb 100644 --- a/docs/examples/1.8.x/console-web/examples/sites/create-vcs-deployment.md +++ b/docs/examples/1.8.x/console-web/examples/sites/create-vcs-deployment.md @@ -1,4 +1,4 @@ -import { Client, Sites, VCSDeploymentType } from "@appwrite.io/console"; +import { Client, Sites, VCSReferenceType } from "@appwrite.io/console"; const client = new Client() .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint @@ -8,7 +8,7 @@ const sites = new Sites(client); const result = await sites.createVcsDeployment({ siteId: '', - type: VCSDeploymentType.Branch, + type: VCSReferenceType.Branch, reference: '', activate: false // optional }); diff --git a/docs/examples/1.8.x/server-dart/examples/functions/create-template-deployment.md b/docs/examples/1.8.x/server-dart/examples/functions/create-template-deployment.md index bfd94b1963..f5b6cdeb1f 100644 --- a/docs/examples/1.8.x/server-dart/examples/functions/create-template-deployment.md +++ b/docs/examples/1.8.x/server-dart/examples/functions/create-template-deployment.md @@ -12,7 +12,7 @@ Deployment result = await functions.createTemplateDeployment( repository: '', owner: '', rootDirectory: '', - type: .commit, + type: TemplateReferenceType.commit, reference: '', activate: false, // (optional) ); diff --git a/docs/examples/1.8.x/server-dart/examples/functions/create-vcs-deployment.md b/docs/examples/1.8.x/server-dart/examples/functions/create-vcs-deployment.md index ed315a54e3..0c12315ffc 100644 --- a/docs/examples/1.8.x/server-dart/examples/functions/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-dart/examples/functions/create-vcs-deployment.md @@ -9,7 +9,7 @@ Functions functions = Functions(client); Deployment result = await functions.createVcsDeployment( functionId: '', - type: VCSDeploymentType.branch, + type: VCSReferenceType.branch, reference: '', activate: false, // (optional) ); diff --git a/docs/examples/1.8.x/server-dart/examples/sites/create-template-deployment.md b/docs/examples/1.8.x/server-dart/examples/sites/create-template-deployment.md index 93c9b1d283..8826b1f462 100644 --- a/docs/examples/1.8.x/server-dart/examples/sites/create-template-deployment.md +++ b/docs/examples/1.8.x/server-dart/examples/sites/create-template-deployment.md @@ -12,7 +12,7 @@ Deployment result = await sites.createTemplateDeployment( repository: '', owner: '', rootDirectory: '', - type: .branch, + type: TemplateReferenceType.branch, reference: '', activate: false, // (optional) ); diff --git a/docs/examples/1.8.x/server-dart/examples/sites/create-vcs-deployment.md b/docs/examples/1.8.x/server-dart/examples/sites/create-vcs-deployment.md index 50f65b9603..52133a535e 100644 --- a/docs/examples/1.8.x/server-dart/examples/sites/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-dart/examples/sites/create-vcs-deployment.md @@ -9,7 +9,7 @@ Sites sites = Sites(client); Deployment result = await sites.createVcsDeployment( siteId: '', - type: VCSDeploymentType.branch, + type: VCSReferenceType.branch, reference: '', activate: false, // (optional) ); diff --git a/docs/examples/1.8.x/server-dotnet/examples/functions/create-template-deployment.md b/docs/examples/1.8.x/server-dotnet/examples/functions/create-template-deployment.md index 125eeda5cc..6fcf2398dd 100644 --- a/docs/examples/1.8.x/server-dotnet/examples/functions/create-template-deployment.md +++ b/docs/examples/1.8.x/server-dotnet/examples/functions/create-template-deployment.md @@ -15,7 +15,7 @@ Deployment result = await functions.CreateTemplateDeployment( repository: "", owner: "", rootDirectory: "", - type: .Commit, + type: TemplateReferenceType.Commit, reference: "", activate: false // optional ); \ No newline at end of file diff --git a/docs/examples/1.8.x/server-dotnet/examples/functions/create-vcs-deployment.md b/docs/examples/1.8.x/server-dotnet/examples/functions/create-vcs-deployment.md index 9651365912..a7403ff116 100644 --- a/docs/examples/1.8.x/server-dotnet/examples/functions/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-dotnet/examples/functions/create-vcs-deployment.md @@ -12,7 +12,7 @@ Functions functions = new Functions(client); Deployment result = await functions.CreateVcsDeployment( functionId: "", - type: VCSDeploymentType.Branch, + type: VCSReferenceType.Branch, reference: "", activate: false // optional ); \ No newline at end of file diff --git a/docs/examples/1.8.x/server-dotnet/examples/sites/create-template-deployment.md b/docs/examples/1.8.x/server-dotnet/examples/sites/create-template-deployment.md index 9775cb659f..5353b004b8 100644 --- a/docs/examples/1.8.x/server-dotnet/examples/sites/create-template-deployment.md +++ b/docs/examples/1.8.x/server-dotnet/examples/sites/create-template-deployment.md @@ -15,7 +15,7 @@ Deployment result = await sites.CreateTemplateDeployment( repository: "", owner: "", rootDirectory: "", - type: .Branch, + type: TemplateReferenceType.Branch, reference: "", activate: false // optional ); \ No newline at end of file diff --git a/docs/examples/1.8.x/server-dotnet/examples/sites/create-vcs-deployment.md b/docs/examples/1.8.x/server-dotnet/examples/sites/create-vcs-deployment.md index 4d3e685176..229549befa 100644 --- a/docs/examples/1.8.x/server-dotnet/examples/sites/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-dotnet/examples/sites/create-vcs-deployment.md @@ -12,7 +12,7 @@ Sites sites = new Sites(client); Deployment result = await sites.CreateVcsDeployment( siteId: "", - type: VCSDeploymentType.Branch, + type: VCSReferenceType.Branch, reference: "", activate: false // optional ); \ No newline at end of file diff --git a/docs/examples/1.8.x/server-kotlin/java/account/create-o-auth-2-token.md b/docs/examples/1.8.x/server-kotlin/java/account/create-o-auth-2-token.md index 5b325f5c61..376d943533 100644 --- a/docs/examples/1.8.x/server-kotlin/java/account/create-o-auth-2-token.md +++ b/docs/examples/1.8.x/server-kotlin/java/account/create-o-auth-2-token.md @@ -13,7 +13,7 @@ account.createOAuth2Token( OAuthProvider.AMAZON, // provider "https://example.com", // success (optional) "https://example.com", // failure (optional) - listOf(), // scopes (optional) + List.of(), // scopes (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/account/list-identities.md b/docs/examples/1.8.x/server-kotlin/java/account/list-identities.md index 8d204d5920..97cdf99b62 100644 --- a/docs/examples/1.8.x/server-kotlin/java/account/list-identities.md +++ b/docs/examples/1.8.x/server-kotlin/java/account/list-identities.md @@ -10,7 +10,7 @@ Client client = new Client() Account account = new Account(client); account.listIdentities( - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/account/list-logs.md b/docs/examples/1.8.x/server-kotlin/java/account/list-logs.md index 4b301a11e0..6c41c7f073 100644 --- a/docs/examples/1.8.x/server-kotlin/java/account/list-logs.md +++ b/docs/examples/1.8.x/server-kotlin/java/account/list-logs.md @@ -10,7 +10,7 @@ Client client = new Client() Account account = new Account(client); account.listLogs( - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/account/update-prefs.md b/docs/examples/1.8.x/server-kotlin/java/account/update-prefs.md index 0b6893916b..4fc1cb5439 100644 --- a/docs/examples/1.8.x/server-kotlin/java/account/update-prefs.md +++ b/docs/examples/1.8.x/server-kotlin/java/account/update-prefs.md @@ -10,10 +10,10 @@ Client client = new Client() Account account = new Account(client); account.updatePrefs( - mapOf( - "language" to "en", - "timezone" to "UTC", - "darkTheme" to true + Map.of( + "language", "en", + "timezone", "UTC", + "darkTheme", true ), // prefs new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/avatars/get-screenshot.md b/docs/examples/1.8.x/server-kotlin/java/avatars/get-screenshot.md index 0f1cb9c2f0..1874358337 100644 --- a/docs/examples/1.8.x/server-kotlin/java/avatars/get-screenshot.md +++ b/docs/examples/1.8.x/server-kotlin/java/avatars/get-screenshot.md @@ -14,7 +14,7 @@ Avatars avatars = new Avatars(client); avatars.getScreenshot( "https://example.com", // url - mapOf( "a" to "b" ), // headers (optional) + Map.of("a", "b"), // headers (optional) 1, // viewportWidth (optional) 1, // viewportHeight (optional) 0.1, // scale (optional) @@ -27,7 +27,7 @@ avatars.getScreenshot( -180, // longitude (optional) 0, // accuracy (optional) false, // touch (optional) - listOf(), // permissions (optional) + List.of(), // permissions (optional) 0, // sleep (optional) 0, // width (optional) 0, // height (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/create-collection.md b/docs/examples/1.8.x/server-kotlin/java/databases/create-collection.md index 10eed04a4f..eea55558cc 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/create-collection.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/create-collection.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.Databases; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Databases; Client client = new Client() .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,7 +15,7 @@ databases.createCollection( "", // databaseId "", // collectionId "", // name - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) false, // documentSecurity (optional) false, // enabled (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/create-document.md b/docs/examples/1.8.x/server-kotlin/java/databases/create-document.md index aa6c9ea203..7ff195f613 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/create-document.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/create-document.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.Databases; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Databases; Client client = new Client() .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,14 +15,14 @@ databases.createDocument( "", // databaseId "", // collectionId "", // documentId - mapOf( - "username" to "walter.obrien", - "email" to "walter.obrien@example.com", - "fullName" to "Walter O'Brien", - "age" to 30, - "isAdmin" to false + Map.of( + "username", "walter.obrien", + "email", "walter.obrien@example.com", + "fullName", "Walter O'Brien", + "age", 30, + "isAdmin", false ), // data - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/create-documents.md b/docs/examples/1.8.x/server-kotlin/java/databases/create-documents.md index 3a4540974b..be695fe80b 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/create-documents.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/create-documents.md @@ -12,7 +12,7 @@ Databases databases = new Databases(client); databases.createDocuments( "", // databaseId "", // collectionId - listOf(), // documents + List.of(), // documents "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/create-enum-attribute.md b/docs/examples/1.8.x/server-kotlin/java/databases/create-enum-attribute.md index 44202086b0..b8666666da 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/create-enum-attribute.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/create-enum-attribute.md @@ -13,7 +13,7 @@ databases.createEnumAttribute( "", // databaseId "", // collectionId "", // key - listOf(), // elements + List.of(), // elements false, // required "", // default (optional) false, // array (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/create-index.md b/docs/examples/1.8.x/server-kotlin/java/databases/create-index.md index fe2d9bf66d..1fb1efb826 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/create-index.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/create-index.md @@ -15,9 +15,9 @@ databases.createIndex( "", // collectionId "", // key IndexType.KEY, // type - listOf(), // attributes - listOf(), // orders (optional) - listOf(), // lengths (optional) + List.of(), // attributes + List.of(), // orders (optional) + List.of(), // lengths (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/create-line-attribute.md b/docs/examples/1.8.x/server-kotlin/java/databases/create-line-attribute.md index ad988b8773..c0daeac6c3 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/create-line-attribute.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/create-line-attribute.md @@ -14,7 +14,7 @@ databases.createLineAttribute( "", // collectionId "", // key false, // required - listOf([1, 2], [3, 4], [5, 6]), // default (optional) + List.of(List.of(1, 2), List.of(3, 4), List.of(5, 6)), // default (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/create-operations.md b/docs/examples/1.8.x/server-kotlin/java/databases/create-operations.md index 2dad8a15ac..c935f82a9a 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/create-operations.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/create-operations.md @@ -11,17 +11,15 @@ Databases databases = new Databases(client); databases.createOperations( "", // transactionId - listOf( - { - "action": "create", - "databaseId": "", - "collectionId": "", - "documentId": "", - "data": { - "name": "Walter O'Brien" - } - } - ), // operations (optional) + List.of(Map.of( + "action", "create", + "databaseId", "", + "collectionId", "", + "documentId", "", + "data", Map.of( + "name", "Walter O'Brien" + ) + )), // operations (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/create-point-attribute.md b/docs/examples/1.8.x/server-kotlin/java/databases/create-point-attribute.md index 89d7cc7177..c3b5d503b2 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/create-point-attribute.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/create-point-attribute.md @@ -14,7 +14,7 @@ databases.createPointAttribute( "", // collectionId "", // key false, // required - listOf(1, 2), // default (optional) + List.of(1, 2), // default (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/create-polygon-attribute.md b/docs/examples/1.8.x/server-kotlin/java/databases/create-polygon-attribute.md index 556fb38481..4f8fe0369e 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/create-polygon-attribute.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/create-polygon-attribute.md @@ -14,7 +14,7 @@ databases.createPolygonAttribute( "", // collectionId "", // key false, // required - listOf([[1, 2], [3, 4], [5, 6], [1, 2]]), // default (optional) + List.of(List.of(List.of(1, 2), List.of(3, 4), List.of(5, 6), List.of(1, 2))), // default (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/delete-documents.md b/docs/examples/1.8.x/server-kotlin/java/databases/delete-documents.md index 958c40c382..f535ae7780 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/delete-documents.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/delete-documents.md @@ -12,7 +12,7 @@ Databases databases = new Databases(client); databases.deleteDocuments( "", // databaseId "", // collectionId - listOf(), // queries (optional) + List.of(), // queries (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/get-document.md b/docs/examples/1.8.x/server-kotlin/java/databases/get-document.md index 489447f599..d3e8b8b3d4 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/get-document.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/get-document.md @@ -13,7 +13,7 @@ databases.getDocument( "", // databaseId "", // collectionId "", // documentId - listOf(), // queries (optional) + List.of(), // queries (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/list-attributes.md b/docs/examples/1.8.x/server-kotlin/java/databases/list-attributes.md index b1b3bd1b9c..dd883e232f 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/list-attributes.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/list-attributes.md @@ -12,7 +12,7 @@ Databases databases = new Databases(client); databases.listAttributes( "", // databaseId "", // collectionId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/list-collections.md b/docs/examples/1.8.x/server-kotlin/java/databases/list-collections.md index efb0e7f89b..ddb47c9989 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/list-collections.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/list-collections.md @@ -11,7 +11,7 @@ Databases databases = new Databases(client); databases.listCollections( "", // databaseId - listOf(), // queries (optional) + List.of(), // queries (optional) "", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/list-documents.md b/docs/examples/1.8.x/server-kotlin/java/databases/list-documents.md index 472d15ba27..b8ef0717ea 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/list-documents.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/list-documents.md @@ -12,7 +12,7 @@ Databases databases = new Databases(client); databases.listDocuments( "", // databaseId "", // collectionId - listOf(), // queries (optional) + List.of(), // queries (optional) "", // transactionId (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/list-indexes.md b/docs/examples/1.8.x/server-kotlin/java/databases/list-indexes.md index 5715af7d47..c701904157 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/list-indexes.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/list-indexes.md @@ -12,7 +12,7 @@ Databases databases = new Databases(client); databases.listIndexes( "", // databaseId "", // collectionId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/list-transactions.md b/docs/examples/1.8.x/server-kotlin/java/databases/list-transactions.md index 281fc1205b..8a6f60544f 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/list-transactions.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/list-transactions.md @@ -10,7 +10,7 @@ Client client = new Client() Databases databases = new Databases(client); databases.listTransactions( - listOf(), // queries (optional) + List.of(), // queries (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/list.md b/docs/examples/1.8.x/server-kotlin/java/databases/list.md index a3f2d51fed..32ef454a73 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/list.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/list.md @@ -10,7 +10,7 @@ Client client = new Client() Databases databases = new Databases(client); databases.list( - listOf(), // queries (optional) + List.of(), // queries (optional) "", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/update-collection.md b/docs/examples/1.8.x/server-kotlin/java/databases/update-collection.md index 24d312da8c..898f1aafe5 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/update-collection.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/update-collection.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.Databases; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Databases; Client client = new Client() .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,7 +15,7 @@ databases.updateCollection( "", // databaseId "", // collectionId "", // name - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) false, // documentSecurity (optional) false, // enabled (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/update-document.md b/docs/examples/1.8.x/server-kotlin/java/databases/update-document.md index 749de99fce..0638225bfd 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/update-document.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/update-document.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.Databases; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Databases; Client client = new Client() .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,8 +15,8 @@ databases.updateDocument( "", // databaseId "", // collectionId "", // documentId - mapOf( "a" to "b" ), // data (optional) - listOf(Permission.read(Role.any())), // permissions (optional) + Map.of("a", "b"), // data (optional) + List.of(Permission.read(Role.any())), // permissions (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/update-documents.md b/docs/examples/1.8.x/server-kotlin/java/databases/update-documents.md index a685ac81fc..57a82f56f9 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/update-documents.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/update-documents.md @@ -12,8 +12,8 @@ Databases databases = new Databases(client); databases.updateDocuments( "", // databaseId "", // collectionId - mapOf( "a" to "b" ), // data (optional) - listOf(), // queries (optional) + Map.of("a", "b"), // data (optional) + List.of(), // queries (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/update-enum-attribute.md b/docs/examples/1.8.x/server-kotlin/java/databases/update-enum-attribute.md index 89606806d9..8870e372a6 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/update-enum-attribute.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/update-enum-attribute.md @@ -13,7 +13,7 @@ databases.updateEnumAttribute( "", // databaseId "", // collectionId "", // key - listOf(), // elements + List.of(), // elements false, // required "", // default "", // newKey (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/update-line-attribute.md b/docs/examples/1.8.x/server-kotlin/java/databases/update-line-attribute.md index 6a4265bbda..14d83eeee5 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/update-line-attribute.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/update-line-attribute.md @@ -14,7 +14,7 @@ databases.updateLineAttribute( "", // collectionId "", // key false, // required - listOf([1, 2], [3, 4], [5, 6]), // default (optional) + List.of(List.of(1, 2), List.of(3, 4), List.of(5, 6)), // default (optional) "", // newKey (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/update-point-attribute.md b/docs/examples/1.8.x/server-kotlin/java/databases/update-point-attribute.md index 38d48c27e8..5d44d7a190 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/update-point-attribute.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/update-point-attribute.md @@ -14,7 +14,7 @@ databases.updatePointAttribute( "", // collectionId "", // key false, // required - listOf(1, 2), // default (optional) + List.of(1, 2), // default (optional) "", // newKey (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/update-polygon-attribute.md b/docs/examples/1.8.x/server-kotlin/java/databases/update-polygon-attribute.md index 6e6fd08575..2c530b9aae 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/update-polygon-attribute.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/update-polygon-attribute.md @@ -14,7 +14,7 @@ databases.updatePolygonAttribute( "", // collectionId "", // key false, // required - listOf([[1, 2], [3, 4], [5, 6], [1, 2]]), // default (optional) + List.of(List.of(List.of(1, 2), List.of(3, 4), List.of(5, 6), List.of(1, 2))), // default (optional) "", // newKey (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/upsert-document.md b/docs/examples/1.8.x/server-kotlin/java/databases/upsert-document.md index 4f156bbf8b..ec99390e15 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/upsert-document.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/upsert-document.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.Databases; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Databases; Client client = new Client() .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,8 +15,8 @@ databases.upsertDocument( "", // databaseId "", // collectionId "", // documentId - mapOf( "a" to "b" ), // data - listOf(Permission.read(Role.any())), // permissions (optional) + Map.of("a", "b"), // data + List.of(Permission.read(Role.any())), // permissions (optional) "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/databases/upsert-documents.md b/docs/examples/1.8.x/server-kotlin/java/databases/upsert-documents.md index b8fcd8781a..ee4450fc29 100644 --- a/docs/examples/1.8.x/server-kotlin/java/databases/upsert-documents.md +++ b/docs/examples/1.8.x/server-kotlin/java/databases/upsert-documents.md @@ -12,7 +12,7 @@ Databases databases = new Databases(client); databases.upsertDocuments( "", // databaseId "", // collectionId - listOf(), // documents + List.of(), // documents "", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/functions/create-execution.md b/docs/examples/1.8.x/server-kotlin/java/functions/create-execution.md index 4abfe236b3..98a4d1b572 100644 --- a/docs/examples/1.8.x/server-kotlin/java/functions/create-execution.md +++ b/docs/examples/1.8.x/server-kotlin/java/functions/create-execution.md @@ -16,7 +16,7 @@ functions.createExecution( false, // async (optional) "", // path (optional) ExecutionMethod.GET, // method (optional) - mapOf( "a" to "b" ), // headers (optional) + Map.of("a", "b"), // headers (optional) "", // scheduledAt (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/functions/create-template-deployment.md b/docs/examples/1.8.x/server-kotlin/java/functions/create-template-deployment.md index 9a0642b04e..59c105dfb1 100644 --- a/docs/examples/1.8.x/server-kotlin/java/functions/create-template-deployment.md +++ b/docs/examples/1.8.x/server-kotlin/java/functions/create-template-deployment.md @@ -1,7 +1,7 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; import io.appwrite.services.Functions; -import io.appwrite.enums.Type; +import io.appwrite.enums.TemplateReferenceType; Client client = new Client() .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,7 +15,7 @@ functions.createTemplateDeployment( "", // repository "", // owner "", // rootDirectory - .COMMIT, // type + TemplateReferenceType.COMMIT, // type "", // reference false, // activate (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/functions/create-vcs-deployment.md b/docs/examples/1.8.x/server-kotlin/java/functions/create-vcs-deployment.md index 9274cd88c7..25f2f28485 100644 --- a/docs/examples/1.8.x/server-kotlin/java/functions/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-kotlin/java/functions/create-vcs-deployment.md @@ -1,7 +1,7 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; import io.appwrite.services.Functions; -import io.appwrite.enums.VCSDeploymentType; +import io.appwrite.enums.VCSReferenceType; Client client = new Client() .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint @@ -12,7 +12,7 @@ Functions functions = new Functions(client); functions.createVcsDeployment( "", // functionId - VCSDeploymentType.BRANCH, // type + VCSReferenceType.BRANCH, // type "", // reference false, // activate (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/functions/create.md b/docs/examples/1.8.x/server-kotlin/java/functions/create.md index 587fd54b9b..17e291140a 100644 --- a/docs/examples/1.8.x/server-kotlin/java/functions/create.md +++ b/docs/examples/1.8.x/server-kotlin/java/functions/create.md @@ -14,15 +14,15 @@ functions.create( "", // functionId "", // name Runtime.NODE_14_5, // runtime - listOf("any"), // execute (optional) - listOf(), // events (optional) + List.of("any"), // execute (optional) + List.of(), // events (optional) "", // schedule (optional) 1, // timeout (optional) false, // enabled (optional) false, // logging (optional) "", // entrypoint (optional) "", // commands (optional) - listOf(), // scopes (optional) + List.of(), // scopes (optional) "", // installationId (optional) "", // providerRepositoryId (optional) "", // providerBranch (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/functions/list-deployments.md b/docs/examples/1.8.x/server-kotlin/java/functions/list-deployments.md index a0ea8b68b3..a92cf0792a 100644 --- a/docs/examples/1.8.x/server-kotlin/java/functions/list-deployments.md +++ b/docs/examples/1.8.x/server-kotlin/java/functions/list-deployments.md @@ -11,7 +11,7 @@ Functions functions = new Functions(client); functions.listDeployments( "", // functionId - listOf(), // queries (optional) + List.of(), // queries (optional) "", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/functions/list-executions.md b/docs/examples/1.8.x/server-kotlin/java/functions/list-executions.md index 8026d4730b..2b97ab3be6 100644 --- a/docs/examples/1.8.x/server-kotlin/java/functions/list-executions.md +++ b/docs/examples/1.8.x/server-kotlin/java/functions/list-executions.md @@ -11,7 +11,7 @@ Functions functions = new Functions(client); functions.listExecutions( "", // functionId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/functions/list.md b/docs/examples/1.8.x/server-kotlin/java/functions/list.md index 5d0f59c3b8..712510db4c 100644 --- a/docs/examples/1.8.x/server-kotlin/java/functions/list.md +++ b/docs/examples/1.8.x/server-kotlin/java/functions/list.md @@ -10,7 +10,7 @@ Client client = new Client() Functions functions = new Functions(client); functions.list( - listOf(), // queries (optional) + List.of(), // queries (optional) "", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/functions/update.md b/docs/examples/1.8.x/server-kotlin/java/functions/update.md index 5cb560e200..4b54a6361d 100644 --- a/docs/examples/1.8.x/server-kotlin/java/functions/update.md +++ b/docs/examples/1.8.x/server-kotlin/java/functions/update.md @@ -14,15 +14,15 @@ functions.update( "", // functionId "", // name Runtime.NODE_14_5, // runtime (optional) - listOf("any"), // execute (optional) - listOf(), // events (optional) + List.of("any"), // execute (optional) + List.of(), // events (optional) "", // schedule (optional) 1, // timeout (optional) false, // enabled (optional) false, // logging (optional) "", // entrypoint (optional) "", // commands (optional) - listOf(), // scopes (optional) + List.of(), // scopes (optional) "", // installationId (optional) "", // providerRepositoryId (optional) "", // providerBranch (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/graphql/mutation.md b/docs/examples/1.8.x/server-kotlin/java/graphql/mutation.md index 778892457b..baf41a8a65 100644 --- a/docs/examples/1.8.x/server-kotlin/java/graphql/mutation.md +++ b/docs/examples/1.8.x/server-kotlin/java/graphql/mutation.md @@ -10,7 +10,7 @@ Client client = new Client() Graphql graphql = new Graphql(client); graphql.mutation( - mapOf( "a" to "b" ), // query + Map.of("a", "b"), // query new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/graphql/query.md b/docs/examples/1.8.x/server-kotlin/java/graphql/query.md index e109d523f8..381da3fb5a 100644 --- a/docs/examples/1.8.x/server-kotlin/java/graphql/query.md +++ b/docs/examples/1.8.x/server-kotlin/java/graphql/query.md @@ -10,7 +10,7 @@ Client client = new Client() Graphql graphql = new Graphql(client); graphql.query( - mapOf( "a" to "b" ), // query + Map.of("a", "b"), // query new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/create-email.md b/docs/examples/1.8.x/server-kotlin/java/messaging/create-email.md index d6ab5ee1bf..ca654e6a08 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/create-email.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/create-email.md @@ -13,12 +13,12 @@ messaging.createEmail( "", // messageId "", // subject "", // content - listOf(), // topics (optional) - listOf(), // users (optional) - listOf(), // targets (optional) - listOf(), // cc (optional) - listOf(), // bcc (optional) - listOf(), // attachments (optional) + List.of(), // topics (optional) + List.of(), // users (optional) + List.of(), // targets (optional) + List.of(), // cc (optional) + List.of(), // bcc (optional) + List.of(), // attachments (optional) false, // draft (optional) false, // html (optional) "", // scheduledAt (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/create-fcm-provider.md b/docs/examples/1.8.x/server-kotlin/java/messaging/create-fcm-provider.md index 0d67e28cf0..554ab3cbcf 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/create-fcm-provider.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/create-fcm-provider.md @@ -12,7 +12,7 @@ Messaging messaging = new Messaging(client); messaging.createFCMProvider( "", // providerId "", // name - mapOf( "a" to "b" ), // serviceAccountJSON (optional) + Map.of("a", "b"), // serviceAccountJSON (optional) false, // enabled (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/create-push.md b/docs/examples/1.8.x/server-kotlin/java/messaging/create-push.md index 94119e6a93..7ab3541a7d 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/create-push.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/create-push.md @@ -14,10 +14,10 @@ messaging.createPush( "", // messageId "", // title (optional) "<BODY>", // body (optional) - listOf(), // topics (optional) - listOf(), // users (optional) - listOf(), // targets (optional) - mapOf( "a" to "b" ), // data (optional) + List.of(), // topics (optional) + List.of(), // users (optional) + List.of(), // targets (optional) + Map.of("a", "b"), // data (optional) "<ACTION>", // action (optional) "<ID1:ID2>", // image (optional) "<ICON>", // icon (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/create-sms.md b/docs/examples/1.8.x/server-kotlin/java/messaging/create-sms.md index ca40cc33a8..5ac95079f7 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/create-sms.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/create-sms.md @@ -12,9 +12,9 @@ Messaging messaging = new Messaging(client); messaging.createSMS( "<MESSAGE_ID>", // messageId "<CONTENT>", // content - listOf(), // topics (optional) - listOf(), // users (optional) - listOf(), // targets (optional) + List.of(), // topics (optional) + List.of(), // users (optional) + List.of(), // targets (optional) false, // draft (optional) "", // scheduledAt (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/create-topic.md b/docs/examples/1.8.x/server-kotlin/java/messaging/create-topic.md index 63a24b467d..ec55053d42 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/create-topic.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/create-topic.md @@ -12,7 +12,7 @@ Messaging messaging = new Messaging(client); messaging.createTopic( "<TOPIC_ID>", // topicId "<NAME>", // name - listOf("any"), // subscribe (optional) + List.of("any"), // subscribe (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/list-message-logs.md b/docs/examples/1.8.x/server-kotlin/java/messaging/list-message-logs.md index 253299ccec..30469ffc09 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/list-message-logs.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/list-message-logs.md @@ -11,7 +11,7 @@ Messaging messaging = new Messaging(client); messaging.listMessageLogs( "<MESSAGE_ID>", // messageId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/list-messages.md b/docs/examples/1.8.x/server-kotlin/java/messaging/list-messages.md index 6535222999..8af8085d8e 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/list-messages.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/list-messages.md @@ -10,7 +10,7 @@ Client client = new Client() Messaging messaging = new Messaging(client); messaging.listMessages( - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/list-provider-logs.md b/docs/examples/1.8.x/server-kotlin/java/messaging/list-provider-logs.md index 3bce21144d..e62dedf89d 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/list-provider-logs.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/list-provider-logs.md @@ -11,7 +11,7 @@ Messaging messaging = new Messaging(client); messaging.listProviderLogs( "<PROVIDER_ID>", // providerId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/list-providers.md b/docs/examples/1.8.x/server-kotlin/java/messaging/list-providers.md index 115cd41745..b52408c02d 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/list-providers.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/list-providers.md @@ -10,7 +10,7 @@ Client client = new Client() Messaging messaging = new Messaging(client); messaging.listProviders( - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/list-subscriber-logs.md b/docs/examples/1.8.x/server-kotlin/java/messaging/list-subscriber-logs.md index e2e1e94667..317db32986 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/list-subscriber-logs.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/list-subscriber-logs.md @@ -11,7 +11,7 @@ Messaging messaging = new Messaging(client); messaging.listSubscriberLogs( "<SUBSCRIBER_ID>", // subscriberId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/list-subscribers.md b/docs/examples/1.8.x/server-kotlin/java/messaging/list-subscribers.md index 3b87ed865d..cc552179de 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/list-subscribers.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/list-subscribers.md @@ -11,7 +11,7 @@ Messaging messaging = new Messaging(client); messaging.listSubscribers( "<TOPIC_ID>", // topicId - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/list-targets.md b/docs/examples/1.8.x/server-kotlin/java/messaging/list-targets.md index db6aee1f68..b123218d22 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/list-targets.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/list-targets.md @@ -11,7 +11,7 @@ Messaging messaging = new Messaging(client); messaging.listTargets( "<MESSAGE_ID>", // messageId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/list-topic-logs.md b/docs/examples/1.8.x/server-kotlin/java/messaging/list-topic-logs.md index 800bb6b7d2..d2d8809575 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/list-topic-logs.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/list-topic-logs.md @@ -11,7 +11,7 @@ Messaging messaging = new Messaging(client); messaging.listTopicLogs( "<TOPIC_ID>", // topicId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/list-topics.md b/docs/examples/1.8.x/server-kotlin/java/messaging/list-topics.md index bd53b41d21..ea258c2759 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/list-topics.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/list-topics.md @@ -10,7 +10,7 @@ Client client = new Client() Messaging messaging = new Messaging(client); messaging.listTopics( - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/update-email.md b/docs/examples/1.8.x/server-kotlin/java/messaging/update-email.md index 56e9767861..1bed63b313 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/update-email.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/update-email.md @@ -11,17 +11,17 @@ Messaging messaging = new Messaging(client); messaging.updateEmail( "<MESSAGE_ID>", // messageId - listOf(), // topics (optional) - listOf(), // users (optional) - listOf(), // targets (optional) + List.of(), // topics (optional) + List.of(), // users (optional) + List.of(), // targets (optional) "<SUBJECT>", // subject (optional) "<CONTENT>", // content (optional) false, // draft (optional) false, // html (optional) - listOf(), // cc (optional) - listOf(), // bcc (optional) + List.of(), // cc (optional) + List.of(), // bcc (optional) "", // scheduledAt (optional) - listOf(), // attachments (optional) + List.of(), // attachments (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/update-fcm-provider.md b/docs/examples/1.8.x/server-kotlin/java/messaging/update-fcm-provider.md index dd92f9321f..2976ff5a53 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/update-fcm-provider.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/update-fcm-provider.md @@ -13,7 +13,7 @@ messaging.updateFCMProvider( "<PROVIDER_ID>", // providerId "<NAME>", // name (optional) false, // enabled (optional) - mapOf( "a" to "b" ), // serviceAccountJSON (optional) + Map.of("a", "b"), // serviceAccountJSON (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/update-push.md b/docs/examples/1.8.x/server-kotlin/java/messaging/update-push.md index 80ad6fcd30..80f70130e5 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/update-push.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/update-push.md @@ -12,12 +12,12 @@ Messaging messaging = new Messaging(client); messaging.updatePush( "<MESSAGE_ID>", // messageId - listOf(), // topics (optional) - listOf(), // users (optional) - listOf(), // targets (optional) + List.of(), // topics (optional) + List.of(), // users (optional) + List.of(), // targets (optional) "<TITLE>", // title (optional) "<BODY>", // body (optional) - mapOf( "a" to "b" ), // data (optional) + Map.of("a", "b"), // data (optional) "<ACTION>", // action (optional) "<ID1:ID2>", // image (optional) "<ICON>", // icon (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/update-sms.md b/docs/examples/1.8.x/server-kotlin/java/messaging/update-sms.md index c59505b68a..4df9588f55 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/update-sms.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/update-sms.md @@ -11,9 +11,9 @@ Messaging messaging = new Messaging(client); messaging.updateSMS( "<MESSAGE_ID>", // messageId - listOf(), // topics (optional) - listOf(), // users (optional) - listOf(), // targets (optional) + List.of(), // topics (optional) + List.of(), // users (optional) + List.of(), // targets (optional) "<CONTENT>", // content (optional) false, // draft (optional) "", // scheduledAt (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/messaging/update-topic.md b/docs/examples/1.8.x/server-kotlin/java/messaging/update-topic.md index be9c44dc23..0d651c7895 100644 --- a/docs/examples/1.8.x/server-kotlin/java/messaging/update-topic.md +++ b/docs/examples/1.8.x/server-kotlin/java/messaging/update-topic.md @@ -12,7 +12,7 @@ Messaging messaging = new Messaging(client); messaging.updateTopic( "<TOPIC_ID>", // topicId "<NAME>", // name (optional) - listOf("any"), // subscribe (optional) + List.of("any"), // subscribe (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/sites/create-template-deployment.md b/docs/examples/1.8.x/server-kotlin/java/sites/create-template-deployment.md index a501461a2c..e7fee585a7 100644 --- a/docs/examples/1.8.x/server-kotlin/java/sites/create-template-deployment.md +++ b/docs/examples/1.8.x/server-kotlin/java/sites/create-template-deployment.md @@ -1,7 +1,7 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; import io.appwrite.services.Sites; -import io.appwrite.enums.Type; +import io.appwrite.enums.TemplateReferenceType; Client client = new Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,7 +15,7 @@ sites.createTemplateDeployment( "<REPOSITORY>", // repository "<OWNER>", // owner "<ROOT_DIRECTORY>", // rootDirectory - .BRANCH, // type + TemplateReferenceType.BRANCH, // type "<REFERENCE>", // reference false, // activate (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/sites/create-vcs-deployment.md b/docs/examples/1.8.x/server-kotlin/java/sites/create-vcs-deployment.md index 754eb26419..8a1dca95cc 100644 --- a/docs/examples/1.8.x/server-kotlin/java/sites/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-kotlin/java/sites/create-vcs-deployment.md @@ -1,7 +1,7 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; import io.appwrite.services.Sites; -import io.appwrite.enums.VCSDeploymentType; +import io.appwrite.enums.VCSReferenceType; Client client = new Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -12,7 +12,7 @@ Sites sites = new Sites(client); sites.createVcsDeployment( "<SITE_ID>", // siteId - VCSDeploymentType.BRANCH, // type + VCSReferenceType.BRANCH, // type "<REFERENCE>", // reference false, // activate (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/sites/list-deployments.md b/docs/examples/1.8.x/server-kotlin/java/sites/list-deployments.md index a19f4ecf09..a1953cdf6e 100644 --- a/docs/examples/1.8.x/server-kotlin/java/sites/list-deployments.md +++ b/docs/examples/1.8.x/server-kotlin/java/sites/list-deployments.md @@ -11,7 +11,7 @@ Sites sites = new Sites(client); sites.listDeployments( "<SITE_ID>", // siteId - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/sites/list-logs.md b/docs/examples/1.8.x/server-kotlin/java/sites/list-logs.md index 85c2171aa2..095f0ae97a 100644 --- a/docs/examples/1.8.x/server-kotlin/java/sites/list-logs.md +++ b/docs/examples/1.8.x/server-kotlin/java/sites/list-logs.md @@ -11,7 +11,7 @@ Sites sites = new Sites(client); sites.listLogs( "<SITE_ID>", // siteId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/sites/list.md b/docs/examples/1.8.x/server-kotlin/java/sites/list.md index 20b5533c7a..d8c69419fe 100644 --- a/docs/examples/1.8.x/server-kotlin/java/sites/list.md +++ b/docs/examples/1.8.x/server-kotlin/java/sites/list.md @@ -10,7 +10,7 @@ Client client = new Client() Sites sites = new Sites(client); sites.list( - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/storage/create-bucket.md b/docs/examples/1.8.x/server-kotlin/java/storage/create-bucket.md index b228408933..0af282a0ca 100644 --- a/docs/examples/1.8.x/server-kotlin/java/storage/create-bucket.md +++ b/docs/examples/1.8.x/server-kotlin/java/storage/create-bucket.md @@ -1,9 +1,9 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.Storage; -import io.appwrite.enums.Compression; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Storage; +import io.appwrite.enums.Compression; Client client = new Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,11 +15,11 @@ Storage storage = new Storage(client); storage.createBucket( "<BUCKET_ID>", // bucketId "<NAME>", // name - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) false, // fileSecurity (optional) false, // enabled (optional) 1, // maximumFileSize (optional) - listOf(), // allowedFileExtensions (optional) + List.of(), // allowedFileExtensions (optional) Compression.NONE, // compression (optional) false, // encryption (optional) false, // antivirus (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/storage/create-file.md b/docs/examples/1.8.x/server-kotlin/java/storage/create-file.md index 7ddf8913e1..abaeee3eca 100644 --- a/docs/examples/1.8.x/server-kotlin/java/storage/create-file.md +++ b/docs/examples/1.8.x/server-kotlin/java/storage/create-file.md @@ -1,9 +1,9 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; import io.appwrite.models.InputFile; -import io.appwrite.services.Storage; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Storage; Client client = new Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -16,7 +16,7 @@ storage.createFile( "<BUCKET_ID>", // bucketId "<FILE_ID>", // fileId InputFile.fromPath("file.png"), // file - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/storage/list-buckets.md b/docs/examples/1.8.x/server-kotlin/java/storage/list-buckets.md index 504cfb7ce2..3fde32c73c 100644 --- a/docs/examples/1.8.x/server-kotlin/java/storage/list-buckets.md +++ b/docs/examples/1.8.x/server-kotlin/java/storage/list-buckets.md @@ -10,7 +10,7 @@ Client client = new Client() Storage storage = new Storage(client); storage.listBuckets( - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/storage/list-files.md b/docs/examples/1.8.x/server-kotlin/java/storage/list-files.md index 6397ca07bc..712c420011 100644 --- a/docs/examples/1.8.x/server-kotlin/java/storage/list-files.md +++ b/docs/examples/1.8.x/server-kotlin/java/storage/list-files.md @@ -11,7 +11,7 @@ Storage storage = new Storage(client); storage.listFiles( "<BUCKET_ID>", // bucketId - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/storage/update-bucket.md b/docs/examples/1.8.x/server-kotlin/java/storage/update-bucket.md index 26758cf19a..47b322e449 100644 --- a/docs/examples/1.8.x/server-kotlin/java/storage/update-bucket.md +++ b/docs/examples/1.8.x/server-kotlin/java/storage/update-bucket.md @@ -1,9 +1,9 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.Storage; -import io.appwrite.enums.Compression; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Storage; +import io.appwrite.enums.Compression; Client client = new Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,11 +15,11 @@ Storage storage = new Storage(client); storage.updateBucket( "<BUCKET_ID>", // bucketId "<NAME>", // name - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) false, // fileSecurity (optional) false, // enabled (optional) 1, // maximumFileSize (optional) - listOf(), // allowedFileExtensions (optional) + List.of(), // allowedFileExtensions (optional) Compression.NONE, // compression (optional) false, // encryption (optional) false, // antivirus (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/storage/update-file.md b/docs/examples/1.8.x/server-kotlin/java/storage/update-file.md index d534e0eb64..b394fd5672 100644 --- a/docs/examples/1.8.x/server-kotlin/java/storage/update-file.md +++ b/docs/examples/1.8.x/server-kotlin/java/storage/update-file.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.Storage; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.Storage; Client client = new Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,7 +15,7 @@ storage.updateFile( "<BUCKET_ID>", // bucketId "<FILE_ID>", // fileId "<NAME>", // name (optional) - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-enum-column.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-enum-column.md index bc31bbe79e..ea0452f250 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-enum-column.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-enum-column.md @@ -13,7 +13,7 @@ tablesDB.createEnumColumn( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId "", // key - listOf(), // elements + List.of(), // elements false, // required "<DEFAULT>", // default (optional) false, // array (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-index.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-index.md index 991bd3429b..07076d9763 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-index.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-index.md @@ -15,9 +15,9 @@ tablesDB.createIndex( "<TABLE_ID>", // tableId "", // key IndexType.KEY, // type - listOf(), // columns - listOf(), // orders (optional) - listOf(), // lengths (optional) + List.of(), // columns + List.of(), // orders (optional) + List.of(), // lengths (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-line-column.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-line-column.md index afe029ebe8..f8351cac79 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-line-column.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-line-column.md @@ -14,7 +14,7 @@ tablesDB.createLineColumn( "<TABLE_ID>", // tableId "", // key false, // required - listOf([1, 2], [3, 4], [5, 6]), // default (optional) + List.of(List.of(1, 2), List.of(3, 4), List.of(5, 6)), // default (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-operations.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-operations.md index 9504f623b3..a7c3a1bcb8 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-operations.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-operations.md @@ -11,17 +11,15 @@ TablesDB tablesDB = new TablesDB(client); tablesDB.createOperations( "<TRANSACTION_ID>", // transactionId - listOf( - { - "action": "create", - "databaseId": "<DATABASE_ID>", - "tableId": "<TABLE_ID>", - "rowId": "<ROW_ID>", - "data": { - "name": "Walter O'Brien" - } - } - ), // operations (optional) + List.of(Map.of( + "action", "create", + "databaseId", "<DATABASE_ID>", + "tableId", "<TABLE_ID>", + "rowId", "<ROW_ID>", + "data", Map.of( + "name", "Walter O'Brien" + ) + )), // operations (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-point-column.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-point-column.md index 2c9941b09c..2f6ae0e937 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-point-column.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-point-column.md @@ -14,7 +14,7 @@ tablesDB.createPointColumn( "<TABLE_ID>", // tableId "", // key false, // required - listOf(1, 2), // default (optional) + List.of(1, 2), // default (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-polygon-column.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-polygon-column.md index 58ca798381..58caa8a791 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-polygon-column.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-polygon-column.md @@ -14,7 +14,7 @@ tablesDB.createPolygonColumn( "<TABLE_ID>", // tableId "", // key false, // required - listOf([[1, 2], [3, 4], [5, 6], [1, 2]]), // default (optional) + List.of(List.of(List.of(1, 2), List.of(3, 4), List.of(5, 6), List.of(1, 2))), // default (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-row.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-row.md index 9e47167cd1..3da8c3248e 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-row.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-row.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.TablesDB; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.TablesDB; Client client = new Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,14 +15,14 @@ tablesDB.createRow( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId "<ROW_ID>", // rowId - mapOf( - "username" to "walter.obrien", - "email" to "walter.obrien@example.com", - "fullName" to "Walter O'Brien", - "age" to 30, - "isAdmin" to false + Map.of( + "username", "walter.obrien", + "email", "walter.obrien@example.com", + "fullName", "Walter O'Brien", + "age", 30, + "isAdmin", false ), // data - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) "<TRANSACTION_ID>", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-rows.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-rows.md index 956d812165..e99ea04da4 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-rows.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-rows.md @@ -12,7 +12,7 @@ TablesDB tablesDB = new TablesDB(client); tablesDB.createRows( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId - listOf(), // rows + List.of(), // rows "<TRANSACTION_ID>", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-table.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-table.md index 1f9fd10d5c..615278a2d4 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-table.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/create-table.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.TablesDB; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.TablesDB; Client client = new Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,7 +15,7 @@ tablesDB.createTable( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId "<NAME>", // name - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) false, // rowSecurity (optional) false, // enabled (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/delete-rows.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/delete-rows.md index 80ca0bb40f..f90789d276 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/delete-rows.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/delete-rows.md @@ -12,7 +12,7 @@ TablesDB tablesDB = new TablesDB(client); tablesDB.deleteRows( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId - listOf(), // queries (optional) + List.of(), // queries (optional) "<TRANSACTION_ID>", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/get-row.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/get-row.md index d642ebcaf1..5b18e8f879 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/get-row.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/get-row.md @@ -13,7 +13,7 @@ tablesDB.getRow( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId "<ROW_ID>", // rowId - listOf(), // queries (optional) + List.of(), // queries (optional) "<TRANSACTION_ID>", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-columns.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-columns.md index bdf376cb33..f7e37f970b 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-columns.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-columns.md @@ -12,7 +12,7 @@ TablesDB tablesDB = new TablesDB(client); tablesDB.listColumns( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-indexes.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-indexes.md index 5b73204314..ca792c5470 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-indexes.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-indexes.md @@ -12,7 +12,7 @@ TablesDB tablesDB = new TablesDB(client); tablesDB.listIndexes( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-rows.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-rows.md index 8d7956bbc6..7903da4b32 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-rows.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-rows.md @@ -12,7 +12,7 @@ TablesDB tablesDB = new TablesDB(client); tablesDB.listRows( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId - listOf(), // queries (optional) + List.of(), // queries (optional) "<TRANSACTION_ID>", // transactionId (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-tables.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-tables.md index 646be0f4a4..9b4c88bdc0 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-tables.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-tables.md @@ -11,7 +11,7 @@ TablesDB tablesDB = new TablesDB(client); tablesDB.listTables( "<DATABASE_ID>", // databaseId - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-transactions.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-transactions.md index acc4902da4..f0b4db05db 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-transactions.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/list-transactions.md @@ -10,7 +10,7 @@ Client client = new Client() TablesDB tablesDB = new TablesDB(client); tablesDB.listTransactions( - listOf(), // queries (optional) + List.of(), // queries (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/list.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/list.md index 98784df806..cf23036591 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/list.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/list.md @@ -10,7 +10,7 @@ Client client = new Client() TablesDB tablesDB = new TablesDB(client); tablesDB.list( - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-enum-column.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-enum-column.md index 5a8036aebc..0f182b76f0 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-enum-column.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-enum-column.md @@ -13,7 +13,7 @@ tablesDB.updateEnumColumn( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId "", // key - listOf(), // elements + List.of(), // elements false, // required "<DEFAULT>", // default "", // newKey (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-line-column.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-line-column.md index 4c65a907f7..4ef648fbc9 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-line-column.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-line-column.md @@ -14,7 +14,7 @@ tablesDB.updateLineColumn( "<TABLE_ID>", // tableId "", // key false, // required - listOf([1, 2], [3, 4], [5, 6]), // default (optional) + List.of(List.of(1, 2), List.of(3, 4), List.of(5, 6)), // default (optional) "", // newKey (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-point-column.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-point-column.md index 56ac86a6f0..729595dfa6 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-point-column.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-point-column.md @@ -14,7 +14,7 @@ tablesDB.updatePointColumn( "<TABLE_ID>", // tableId "", // key false, // required - listOf(1, 2), // default (optional) + List.of(1, 2), // default (optional) "", // newKey (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-polygon-column.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-polygon-column.md index 189d473175..bc6ccc93b5 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-polygon-column.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-polygon-column.md @@ -14,7 +14,7 @@ tablesDB.updatePolygonColumn( "<TABLE_ID>", // tableId "", // key false, // required - listOf([[1, 2], [3, 4], [5, 6], [1, 2]]), // default (optional) + List.of(List.of(List.of(1, 2), List.of(3, 4), List.of(5, 6), List.of(1, 2))), // default (optional) "", // newKey (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-row.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-row.md index 835f63b19a..5585274cbb 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-row.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-row.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.TablesDB; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.TablesDB; Client client = new Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,8 +15,8 @@ tablesDB.updateRow( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId "<ROW_ID>", // rowId - mapOf( "a" to "b" ), // data (optional) - listOf(Permission.read(Role.any())), // permissions (optional) + Map.of("a", "b"), // data (optional) + List.of(Permission.read(Role.any())), // permissions (optional) "<TRANSACTION_ID>", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-rows.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-rows.md index 7d39e4422c..b479613856 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-rows.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-rows.md @@ -12,8 +12,8 @@ TablesDB tablesDB = new TablesDB(client); tablesDB.updateRows( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId - mapOf( "a" to "b" ), // data (optional) - listOf(), // queries (optional) + Map.of("a", "b"), // data (optional) + List.of(), // queries (optional) "<TRANSACTION_ID>", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-table.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-table.md index 257803b984..cf0c2fbc2b 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-table.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/update-table.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.TablesDB; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.TablesDB; Client client = new Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,7 +15,7 @@ tablesDB.updateTable( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId "<NAME>", // name - listOf(Permission.read(Role.any())), // permissions (optional) + List.of(Permission.read(Role.any())), // permissions (optional) false, // rowSecurity (optional) false, // enabled (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/upsert-row.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/upsert-row.md index 6ea29e3e8d..adb2095f34 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/upsert-row.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/upsert-row.md @@ -1,8 +1,8 @@ import io.appwrite.Client; import io.appwrite.coroutines.CoroutineCallback; -import io.appwrite.services.TablesDB; import io.appwrite.Permission; import io.appwrite.Role; +import io.appwrite.services.TablesDB; Client client = new Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,8 +15,8 @@ tablesDB.upsertRow( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId "<ROW_ID>", // rowId - mapOf( "a" to "b" ), // data (optional) - listOf(Permission.read(Role.any())), // permissions (optional) + Map.of("a", "b"), // data (optional) + List.of(Permission.read(Role.any())), // permissions (optional) "<TRANSACTION_ID>", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/tablesdb/upsert-rows.md b/docs/examples/1.8.x/server-kotlin/java/tablesdb/upsert-rows.md index c4b2bf3857..e16ecb35c9 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tablesdb/upsert-rows.md +++ b/docs/examples/1.8.x/server-kotlin/java/tablesdb/upsert-rows.md @@ -12,7 +12,7 @@ TablesDB tablesDB = new TablesDB(client); tablesDB.upsertRows( "<DATABASE_ID>", // databaseId "<TABLE_ID>", // tableId - listOf(), // rows + List.of(), // rows "<TRANSACTION_ID>", // transactionId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/teams/create-membership.md b/docs/examples/1.8.x/server-kotlin/java/teams/create-membership.md index 89e9d96ef6..e71ea2e578 100644 --- a/docs/examples/1.8.x/server-kotlin/java/teams/create-membership.md +++ b/docs/examples/1.8.x/server-kotlin/java/teams/create-membership.md @@ -11,7 +11,7 @@ Teams teams = new Teams(client); teams.createMembership( "<TEAM_ID>", // teamId - listOf(), // roles + List.of(), // roles "email@example.com", // email (optional) "<USER_ID>", // userId (optional) "+12065550100", // phone (optional) diff --git a/docs/examples/1.8.x/server-kotlin/java/teams/create.md b/docs/examples/1.8.x/server-kotlin/java/teams/create.md index 28cc3dada1..bc82cb47e1 100644 --- a/docs/examples/1.8.x/server-kotlin/java/teams/create.md +++ b/docs/examples/1.8.x/server-kotlin/java/teams/create.md @@ -12,7 +12,7 @@ Teams teams = new Teams(client); teams.create( "<TEAM_ID>", // teamId "<NAME>", // name - listOf(), // roles (optional) + List.of(), // roles (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/teams/list-memberships.md b/docs/examples/1.8.x/server-kotlin/java/teams/list-memberships.md index bfbf519db8..5437e6048c 100644 --- a/docs/examples/1.8.x/server-kotlin/java/teams/list-memberships.md +++ b/docs/examples/1.8.x/server-kotlin/java/teams/list-memberships.md @@ -11,7 +11,7 @@ Teams teams = new Teams(client); teams.listMemberships( "<TEAM_ID>", // teamId - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/teams/list.md b/docs/examples/1.8.x/server-kotlin/java/teams/list.md index 7ff98ad86f..06f0034bfe 100644 --- a/docs/examples/1.8.x/server-kotlin/java/teams/list.md +++ b/docs/examples/1.8.x/server-kotlin/java/teams/list.md @@ -10,7 +10,7 @@ Client client = new Client() Teams teams = new Teams(client); teams.list( - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/teams/update-membership.md b/docs/examples/1.8.x/server-kotlin/java/teams/update-membership.md index d4816c57f1..2e0de4e3b0 100644 --- a/docs/examples/1.8.x/server-kotlin/java/teams/update-membership.md +++ b/docs/examples/1.8.x/server-kotlin/java/teams/update-membership.md @@ -12,7 +12,7 @@ Teams teams = new Teams(client); teams.updateMembership( "<TEAM_ID>", // teamId "<MEMBERSHIP_ID>", // membershipId - listOf(), // roles + List.of(), // roles new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/teams/update-prefs.md b/docs/examples/1.8.x/server-kotlin/java/teams/update-prefs.md index 2ef05222df..85f18aef1e 100644 --- a/docs/examples/1.8.x/server-kotlin/java/teams/update-prefs.md +++ b/docs/examples/1.8.x/server-kotlin/java/teams/update-prefs.md @@ -11,7 +11,7 @@ Teams teams = new Teams(client); teams.updatePrefs( "<TEAM_ID>", // teamId - mapOf( "a" to "b" ), // prefs + Map.of("a", "b"), // prefs new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/tokens/list.md b/docs/examples/1.8.x/server-kotlin/java/tokens/list.md index 23c51a64ef..1147f6f536 100644 --- a/docs/examples/1.8.x/server-kotlin/java/tokens/list.md +++ b/docs/examples/1.8.x/server-kotlin/java/tokens/list.md @@ -12,7 +12,7 @@ Tokens tokens = new Tokens(client); tokens.list( "<BUCKET_ID>", // bucketId "<FILE_ID>", // fileId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/users/list-identities.md b/docs/examples/1.8.x/server-kotlin/java/users/list-identities.md index fc95c8e48c..2dfa297592 100644 --- a/docs/examples/1.8.x/server-kotlin/java/users/list-identities.md +++ b/docs/examples/1.8.x/server-kotlin/java/users/list-identities.md @@ -10,7 +10,7 @@ Client client = new Client() Users users = new Users(client); users.listIdentities( - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/users/list-logs.md b/docs/examples/1.8.x/server-kotlin/java/users/list-logs.md index 4a2e549ed4..ebaa749f56 100644 --- a/docs/examples/1.8.x/server-kotlin/java/users/list-logs.md +++ b/docs/examples/1.8.x/server-kotlin/java/users/list-logs.md @@ -11,7 +11,7 @@ Users users = new Users(client); users.listLogs( "<USER_ID>", // userId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/users/list-memberships.md b/docs/examples/1.8.x/server-kotlin/java/users/list-memberships.md index 36e67ae322..3335e18bc7 100644 --- a/docs/examples/1.8.x/server-kotlin/java/users/list-memberships.md +++ b/docs/examples/1.8.x/server-kotlin/java/users/list-memberships.md @@ -11,7 +11,7 @@ Users users = new Users(client); users.listMemberships( "<USER_ID>", // userId - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/users/list-targets.md b/docs/examples/1.8.x/server-kotlin/java/users/list-targets.md index 156aaefa7e..02fd291cb5 100644 --- a/docs/examples/1.8.x/server-kotlin/java/users/list-targets.md +++ b/docs/examples/1.8.x/server-kotlin/java/users/list-targets.md @@ -11,7 +11,7 @@ Users users = new Users(client); users.listTargets( "<USER_ID>", // userId - listOf(), // queries (optional) + List.of(), // queries (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/1.8.x/server-kotlin/java/users/list.md b/docs/examples/1.8.x/server-kotlin/java/users/list.md index ec038afded..65ed4b00f8 100644 --- a/docs/examples/1.8.x/server-kotlin/java/users/list.md +++ b/docs/examples/1.8.x/server-kotlin/java/users/list.md @@ -10,7 +10,7 @@ Client client = new Client() Users users = new Users(client); users.list( - listOf(), // queries (optional) + List.of(), // queries (optional) "<SEARCH>", // search (optional) false, // total (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/1.8.x/server-kotlin/java/users/update-labels.md b/docs/examples/1.8.x/server-kotlin/java/users/update-labels.md index 379200a56b..953f466290 100644 --- a/docs/examples/1.8.x/server-kotlin/java/users/update-labels.md +++ b/docs/examples/1.8.x/server-kotlin/java/users/update-labels.md @@ -11,7 +11,7 @@ Users users = new Users(client); users.updateLabels( "<USER_ID>", // userId - listOf(), // labels + List.of(), // labels new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/java/users/update-prefs.md b/docs/examples/1.8.x/server-kotlin/java/users/update-prefs.md index c5a9677a20..5a128aa716 100644 --- a/docs/examples/1.8.x/server-kotlin/java/users/update-prefs.md +++ b/docs/examples/1.8.x/server-kotlin/java/users/update-prefs.md @@ -11,7 +11,7 @@ Users users = new Users(client); users.updatePrefs( "<USER_ID>", // userId - mapOf( "a" to "b" ), // prefs + Map.of("a", "b"), // prefs new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/databases/create-line-attribute.md b/docs/examples/1.8.x/server-kotlin/kotlin/databases/create-line-attribute.md index af9a4d2425..8f1322b3fd 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/databases/create-line-attribute.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/databases/create-line-attribute.md @@ -14,5 +14,5 @@ val response = databases.createLineAttribute( collectionId = "<COLLECTION_ID>", key = "", required = false, - default = listOf([1, 2], [3, 4], [5, 6]) // optional + default = listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6)) // optional ) diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/databases/create-operations.md b/docs/examples/1.8.x/server-kotlin/kotlin/databases/create-operations.md index 1c741818b9..eae10ab609 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/databases/create-operations.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/databases/create-operations.md @@ -11,15 +11,13 @@ val databases = Databases(client) val response = databases.createOperations( transactionId = "<TRANSACTION_ID>", - operations = listOf( - { - "action": "create", - "databaseId": "<DATABASE_ID>", - "collectionId": "<COLLECTION_ID>", - "documentId": "<DOCUMENT_ID>", - "data": { - "name": "Walter O'Brien" - } - } - ) // optional + operations = listOf(mapOf( + "action" to "create", + "databaseId" to "<DATABASE_ID>", + "collectionId" to "<COLLECTION_ID>", + "documentId" to "<DOCUMENT_ID>", + "data" to mapOf( + "name" to "Walter O'Brien" + ) + )) // optional ) diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/databases/create-polygon-attribute.md b/docs/examples/1.8.x/server-kotlin/kotlin/databases/create-polygon-attribute.md index ffeb3c4398..5a3491c414 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/databases/create-polygon-attribute.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/databases/create-polygon-attribute.md @@ -14,5 +14,5 @@ val response = databases.createPolygonAttribute( collectionId = "<COLLECTION_ID>", key = "", required = false, - default = listOf([[1, 2], [3, 4], [5, 6], [1, 2]]) // optional + default = listOf(listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6), listOf(1, 2))) // optional ) diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/databases/update-line-attribute.md b/docs/examples/1.8.x/server-kotlin/kotlin/databases/update-line-attribute.md index 0d6b40a2d8..0a8b50a332 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/databases/update-line-attribute.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/databases/update-line-attribute.md @@ -14,6 +14,6 @@ val response = databases.updateLineAttribute( collectionId = "<COLLECTION_ID>", key = "", required = false, - default = listOf([1, 2], [3, 4], [5, 6]), // optional + default = listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6)), // optional newKey = "" // optional ) diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/databases/update-polygon-attribute.md b/docs/examples/1.8.x/server-kotlin/kotlin/databases/update-polygon-attribute.md index 66bbdea11c..37df6acee0 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/databases/update-polygon-attribute.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/databases/update-polygon-attribute.md @@ -14,6 +14,6 @@ val response = databases.updatePolygonAttribute( collectionId = "<COLLECTION_ID>", key = "", required = false, - default = listOf([[1, 2], [3, 4], [5, 6], [1, 2]]), // optional + default = listOf(listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6), listOf(1, 2))), // optional newKey = "" // optional ) diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/functions/create-template-deployment.md b/docs/examples/1.8.x/server-kotlin/kotlin/functions/create-template-deployment.md index b8eb44b4fa..176620e19d 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/functions/create-template-deployment.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/functions/create-template-deployment.md @@ -1,7 +1,7 @@ import io.appwrite.Client import io.appwrite.coroutines.CoroutineCallback import io.appwrite.services.Functions -import io.appwrite.enums.Type +import io.appwrite.enums.TemplateReferenceType val client = Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,7 +15,7 @@ val response = functions.createTemplateDeployment( repository = "<REPOSITORY>", owner = "<OWNER>", rootDirectory = "<ROOT_DIRECTORY>", - type = .COMMIT, + type = TemplateReferenceType.COMMIT, reference = "<REFERENCE>", activate = false // optional ) diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/functions/create-vcs-deployment.md b/docs/examples/1.8.x/server-kotlin/kotlin/functions/create-vcs-deployment.md index 08bb5a3097..4e1c21daeb 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/functions/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/functions/create-vcs-deployment.md @@ -1,7 +1,7 @@ import io.appwrite.Client import io.appwrite.coroutines.CoroutineCallback import io.appwrite.services.Functions -import io.appwrite.enums.VCSDeploymentType +import io.appwrite.enums.VCSReferenceType val client = Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -12,7 +12,7 @@ val functions = Functions(client) val response = functions.createVcsDeployment( functionId = "<FUNCTION_ID>", - type = VCSDeploymentType.BRANCH, + type = VCSReferenceType.BRANCH, reference = "<REFERENCE>", activate = false // optional ) diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/sites/create-template-deployment.md b/docs/examples/1.8.x/server-kotlin/kotlin/sites/create-template-deployment.md index 1f3d7d386a..6ef7414bc4 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/sites/create-template-deployment.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/sites/create-template-deployment.md @@ -1,7 +1,7 @@ import io.appwrite.Client import io.appwrite.coroutines.CoroutineCallback import io.appwrite.services.Sites -import io.appwrite.enums.Type +import io.appwrite.enums.TemplateReferenceType val client = Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -15,7 +15,7 @@ val response = sites.createTemplateDeployment( repository = "<REPOSITORY>", owner = "<OWNER>", rootDirectory = "<ROOT_DIRECTORY>", - type = .BRANCH, + type = TemplateReferenceType.BRANCH, reference = "<REFERENCE>", activate = false // optional ) diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/sites/create-vcs-deployment.md b/docs/examples/1.8.x/server-kotlin/kotlin/sites/create-vcs-deployment.md index 141cf3e658..c11e890fc0 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/sites/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/sites/create-vcs-deployment.md @@ -1,7 +1,7 @@ import io.appwrite.Client import io.appwrite.coroutines.CoroutineCallback import io.appwrite.services.Sites -import io.appwrite.enums.VCSDeploymentType +import io.appwrite.enums.VCSReferenceType val client = Client() .setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint @@ -12,7 +12,7 @@ val sites = Sites(client) val response = sites.createVcsDeployment( siteId = "<SITE_ID>", - type = VCSDeploymentType.BRANCH, + type = VCSReferenceType.BRANCH, reference = "<REFERENCE>", activate = false // optional ) diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/create-line-column.md b/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/create-line-column.md index 3dd9ebd083..cede289439 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/create-line-column.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/create-line-column.md @@ -14,5 +14,5 @@ val response = tablesDB.createLineColumn( tableId = "<TABLE_ID>", key = "", required = false, - default = listOf([1, 2], [3, 4], [5, 6]) // optional + default = listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6)) // optional ) diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/create-operations.md b/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/create-operations.md index 40c98d1c81..5b05949715 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/create-operations.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/create-operations.md @@ -11,15 +11,13 @@ val tablesDB = TablesDB(client) val response = tablesDB.createOperations( transactionId = "<TRANSACTION_ID>", - operations = listOf( - { - "action": "create", - "databaseId": "<DATABASE_ID>", - "tableId": "<TABLE_ID>", - "rowId": "<ROW_ID>", - "data": { - "name": "Walter O'Brien" - } - } - ) // optional + operations = listOf(mapOf( + "action" to "create", + "databaseId" to "<DATABASE_ID>", + "tableId" to "<TABLE_ID>", + "rowId" to "<ROW_ID>", + "data" to mapOf( + "name" to "Walter O'Brien" + ) + )) // optional ) diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/create-polygon-column.md b/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/create-polygon-column.md index 218b4cba33..c4282d1b67 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/create-polygon-column.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/create-polygon-column.md @@ -14,5 +14,5 @@ val response = tablesDB.createPolygonColumn( tableId = "<TABLE_ID>", key = "", required = false, - default = listOf([[1, 2], [3, 4], [5, 6], [1, 2]]) // optional + default = listOf(listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6), listOf(1, 2))) // optional ) diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/update-line-column.md b/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/update-line-column.md index 571d25206c..4b975c0ee0 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/update-line-column.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/update-line-column.md @@ -14,6 +14,6 @@ val response = tablesDB.updateLineColumn( tableId = "<TABLE_ID>", key = "", required = false, - default = listOf([1, 2], [3, 4], [5, 6]), // optional + default = listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6)), // optional newKey = "" // optional ) diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/update-polygon-column.md b/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/update-polygon-column.md index db3a46bc6c..4042847176 100644 --- a/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/update-polygon-column.md +++ b/docs/examples/1.8.x/server-kotlin/kotlin/tablesdb/update-polygon-column.md @@ -14,6 +14,6 @@ val response = tablesDB.updatePolygonColumn( tableId = "<TABLE_ID>", key = "", required = false, - default = listOf([[1, 2], [3, 4], [5, 6], [1, 2]]), // optional + default = listOf(listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6), listOf(1, 2))), // optional newKey = "" // optional ) diff --git a/docs/examples/1.8.x/server-nodejs/examples/functions/create-template-deployment.md b/docs/examples/1.8.x/server-nodejs/examples/functions/create-template-deployment.md index 999c029f8f..f1efd7b199 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/functions/create-template-deployment.md +++ b/docs/examples/1.8.x/server-nodejs/examples/functions/create-template-deployment.md @@ -12,7 +12,7 @@ const result = await functions.createTemplateDeployment({ repository: '<REPOSITORY>', owner: '<OWNER>', rootDirectory: '<ROOT_DIRECTORY>', - type: sdk..Commit, + type: sdk.TemplateReferenceType.Commit, reference: '<REFERENCE>', activate: false // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/functions/create-vcs-deployment.md b/docs/examples/1.8.x/server-nodejs/examples/functions/create-vcs-deployment.md index 0aabfcff8a..c648625531 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/functions/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-nodejs/examples/functions/create-vcs-deployment.md @@ -9,7 +9,7 @@ const functions = new sdk.Functions(client); const result = await functions.createVcsDeployment({ functionId: '<FUNCTION_ID>', - type: sdk.VCSDeploymentType.Branch, + type: sdk.VCSReferenceType.Branch, reference: '<REFERENCE>', activate: false // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/sites/create-template-deployment.md b/docs/examples/1.8.x/server-nodejs/examples/sites/create-template-deployment.md index dc9736fa80..3728f7f846 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/sites/create-template-deployment.md +++ b/docs/examples/1.8.x/server-nodejs/examples/sites/create-template-deployment.md @@ -12,7 +12,7 @@ const result = await sites.createTemplateDeployment({ repository: '<REPOSITORY>', owner: '<OWNER>', rootDirectory: '<ROOT_DIRECTORY>', - type: sdk..Branch, + type: sdk.TemplateReferenceType.Branch, reference: '<REFERENCE>', activate: false // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/sites/create-vcs-deployment.md b/docs/examples/1.8.x/server-nodejs/examples/sites/create-vcs-deployment.md index 7f7c2196ba..95cefea9ae 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/sites/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-nodejs/examples/sites/create-vcs-deployment.md @@ -9,7 +9,7 @@ const sites = new sdk.Sites(client); const result = await sites.createVcsDeployment({ siteId: '<SITE_ID>', - type: sdk.VCSDeploymentType.Branch, + type: sdk.VCSReferenceType.Branch, reference: '<REFERENCE>', activate: false // optional }); diff --git a/docs/examples/1.8.x/server-php/examples/functions/create-template-deployment.md b/docs/examples/1.8.x/server-php/examples/functions/create-template-deployment.md index 5888396312..a1272523f7 100644 --- a/docs/examples/1.8.x/server-php/examples/functions/create-template-deployment.md +++ b/docs/examples/1.8.x/server-php/examples/functions/create-template-deployment.md @@ -2,7 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Functions; -use Appwrite\Enums\Type; +use Appwrite\Enums\TemplateReferenceType; $client = (new Client()) ->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint @@ -16,7 +16,7 @@ $result = $functions->createTemplateDeployment( repository: '<REPOSITORY>', owner: '<OWNER>', rootDirectory: '<ROOT_DIRECTORY>', - type: Type::COMMIT(), + type: TemplateReferenceType::COMMIT(), reference: '<REFERENCE>', activate: false // optional ); \ No newline at end of file diff --git a/docs/examples/1.8.x/server-php/examples/functions/create-vcs-deployment.md b/docs/examples/1.8.x/server-php/examples/functions/create-vcs-deployment.md index bb4622e67a..f044219541 100644 --- a/docs/examples/1.8.x/server-php/examples/functions/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-php/examples/functions/create-vcs-deployment.md @@ -2,7 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Functions; -use Appwrite\Enums\VCSDeploymentType; +use Appwrite\Enums\VCSReferenceType; $client = (new Client()) ->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint @@ -13,7 +13,7 @@ $functions = new Functions($client); $result = $functions->createVcsDeployment( functionId: '<FUNCTION_ID>', - type: VCSDeploymentType::BRANCH(), + type: VCSReferenceType::BRANCH(), reference: '<REFERENCE>', activate: false // optional ); \ No newline at end of file diff --git a/docs/examples/1.8.x/server-php/examples/sites/create-template-deployment.md b/docs/examples/1.8.x/server-php/examples/sites/create-template-deployment.md index 314ffdd1b9..1661bbb407 100644 --- a/docs/examples/1.8.x/server-php/examples/sites/create-template-deployment.md +++ b/docs/examples/1.8.x/server-php/examples/sites/create-template-deployment.md @@ -2,7 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Sites; -use Appwrite\Enums\Type; +use Appwrite\Enums\TemplateReferenceType; $client = (new Client()) ->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint @@ -16,7 +16,7 @@ $result = $sites->createTemplateDeployment( repository: '<REPOSITORY>', owner: '<OWNER>', rootDirectory: '<ROOT_DIRECTORY>', - type: Type::BRANCH(), + type: TemplateReferenceType::BRANCH(), reference: '<REFERENCE>', activate: false // optional ); \ No newline at end of file diff --git a/docs/examples/1.8.x/server-php/examples/sites/create-vcs-deployment.md b/docs/examples/1.8.x/server-php/examples/sites/create-vcs-deployment.md index 7f63dffdf8..015bf09d7d 100644 --- a/docs/examples/1.8.x/server-php/examples/sites/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-php/examples/sites/create-vcs-deployment.md @@ -2,7 +2,7 @@ use Appwrite\Client; use Appwrite\Services\Sites; -use Appwrite\Enums\VCSDeploymentType; +use Appwrite\Enums\VCSReferenceType; $client = (new Client()) ->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint @@ -13,7 +13,7 @@ $sites = new Sites($client); $result = $sites->createVcsDeployment( siteId: '<SITE_ID>', - type: VCSDeploymentType::BRANCH(), + type: VCSReferenceType::BRANCH(), reference: '<REFERENCE>', activate: false // optional ); \ No newline at end of file diff --git a/docs/examples/1.8.x/server-python/examples/functions/create-template-deployment.md b/docs/examples/1.8.x/server-python/examples/functions/create-template-deployment.md index d9f8f70a82..db2058a87a 100644 --- a/docs/examples/1.8.x/server-python/examples/functions/create-template-deployment.md +++ b/docs/examples/1.8.x/server-python/examples/functions/create-template-deployment.md @@ -1,6 +1,6 @@ from appwrite.client import Client from appwrite.services.functions import Functions -from appwrite.enums import +from appwrite.enums import TemplateReferenceType client = Client() client.set_endpoint('https://<REGION>.cloud.appwrite.io/v1') # Your API Endpoint @@ -14,7 +14,7 @@ result = functions.create_template_deployment( repository = '<REPOSITORY>', owner = '<OWNER>', root_directory = '<ROOT_DIRECTORY>', - type = .COMMIT, + type = TemplateReferenceType.COMMIT, reference = '<REFERENCE>', activate = False # optional ) diff --git a/docs/examples/1.8.x/server-python/examples/functions/create-vcs-deployment.md b/docs/examples/1.8.x/server-python/examples/functions/create-vcs-deployment.md index 4004baec27..43e198b4b8 100644 --- a/docs/examples/1.8.x/server-python/examples/functions/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-python/examples/functions/create-vcs-deployment.md @@ -1,6 +1,6 @@ from appwrite.client import Client from appwrite.services.functions import Functions -from appwrite.enums import VCSDeploymentType +from appwrite.enums import VCSReferenceType client = Client() client.set_endpoint('https://<REGION>.cloud.appwrite.io/v1') # Your API Endpoint @@ -11,7 +11,7 @@ functions = Functions(client) result = functions.create_vcs_deployment( function_id = '<FUNCTION_ID>', - type = VCSDeploymentType.BRANCH, + type = VCSReferenceType.BRANCH, reference = '<REFERENCE>', activate = False # optional ) diff --git a/docs/examples/1.8.x/server-python/examples/sites/create-template-deployment.md b/docs/examples/1.8.x/server-python/examples/sites/create-template-deployment.md index fb47cb1453..700ca44d1b 100644 --- a/docs/examples/1.8.x/server-python/examples/sites/create-template-deployment.md +++ b/docs/examples/1.8.x/server-python/examples/sites/create-template-deployment.md @@ -1,6 +1,6 @@ from appwrite.client import Client from appwrite.services.sites import Sites -from appwrite.enums import +from appwrite.enums import TemplateReferenceType client = Client() client.set_endpoint('https://<REGION>.cloud.appwrite.io/v1') # Your API Endpoint @@ -14,7 +14,7 @@ result = sites.create_template_deployment( repository = '<REPOSITORY>', owner = '<OWNER>', root_directory = '<ROOT_DIRECTORY>', - type = .BRANCH, + type = TemplateReferenceType.BRANCH, reference = '<REFERENCE>', activate = False # optional ) diff --git a/docs/examples/1.8.x/server-python/examples/sites/create-vcs-deployment.md b/docs/examples/1.8.x/server-python/examples/sites/create-vcs-deployment.md index 089e6c8141..ec02f31c00 100644 --- a/docs/examples/1.8.x/server-python/examples/sites/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-python/examples/sites/create-vcs-deployment.md @@ -1,6 +1,6 @@ from appwrite.client import Client from appwrite.services.sites import Sites -from appwrite.enums import VCSDeploymentType +from appwrite.enums import VCSReferenceType client = Client() client.set_endpoint('https://<REGION>.cloud.appwrite.io/v1') # Your API Endpoint @@ -11,7 +11,7 @@ sites = Sites(client) result = sites.create_vcs_deployment( site_id = '<SITE_ID>', - type = VCSDeploymentType.BRANCH, + type = VCSReferenceType.BRANCH, reference = '<REFERENCE>', activate = False # optional ) diff --git a/docs/examples/1.8.x/server-rest/examples/account/create-mfa-challenge.md b/docs/examples/1.8.x/server-rest/examples/account/create-mfa-challenge.md index dd5ef4c731..e5a5b0ea05 100644 --- a/docs/examples/1.8.x/server-rest/examples/account/create-mfa-challenge.md +++ b/docs/examples/1.8.x/server-rest/examples/account/create-mfa-challenge.md @@ -1,4 +1,4 @@ -POST /v1/account/mfa/challenge HTTP/1.1 +POST /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.8.0 diff --git a/docs/examples/1.8.x/server-rest/examples/account/update-mfa-challenge.md b/docs/examples/1.8.x/server-rest/examples/account/update-mfa-challenge.md index b6a7e92b28..df2cd9a1e8 100644 --- a/docs/examples/1.8.x/server-rest/examples/account/update-mfa-challenge.md +++ b/docs/examples/1.8.x/server-rest/examples/account/update-mfa-challenge.md @@ -1,4 +1,4 @@ -PUT /v1/account/mfa/challenge HTTP/1.1 +PUT /v1/account/mfa/challenges HTTP/1.1 Host: cloud.appwrite.io Content-Type: application/json X-Appwrite-Response-Format: 1.8.0 diff --git a/docs/examples/1.8.x/server-ruby/examples/functions/create-template-deployment.md b/docs/examples/1.8.x/server-ruby/examples/functions/create-template-deployment.md index cfe0adc263..e4c759f3fc 100644 --- a/docs/examples/1.8.x/server-ruby/examples/functions/create-template-deployment.md +++ b/docs/examples/1.8.x/server-ruby/examples/functions/create-template-deployment.md @@ -15,7 +15,7 @@ result = functions.create_template_deployment( repository: '<REPOSITORY>', owner: '<OWNER>', root_directory: '<ROOT_DIRECTORY>', - type: ::COMMIT, + type: TemplateReferenceType::COMMIT, reference: '<REFERENCE>', activate: false # optional ) diff --git a/docs/examples/1.8.x/server-ruby/examples/functions/create-vcs-deployment.md b/docs/examples/1.8.x/server-ruby/examples/functions/create-vcs-deployment.md index 75bd3c49f5..930ec6dc76 100644 --- a/docs/examples/1.8.x/server-ruby/examples/functions/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-ruby/examples/functions/create-vcs-deployment.md @@ -12,7 +12,7 @@ functions = Functions.new(client) result = functions.create_vcs_deployment( function_id: '<FUNCTION_ID>', - type: VCSDeploymentType::BRANCH, + type: VCSReferenceType::BRANCH, reference: '<REFERENCE>', activate: false # optional ) diff --git a/docs/examples/1.8.x/server-ruby/examples/sites/create-template-deployment.md b/docs/examples/1.8.x/server-ruby/examples/sites/create-template-deployment.md index 9e1cc93986..4fb81779fe 100644 --- a/docs/examples/1.8.x/server-ruby/examples/sites/create-template-deployment.md +++ b/docs/examples/1.8.x/server-ruby/examples/sites/create-template-deployment.md @@ -15,7 +15,7 @@ result = sites.create_template_deployment( repository: '<REPOSITORY>', owner: '<OWNER>', root_directory: '<ROOT_DIRECTORY>', - type: ::BRANCH, + type: TemplateReferenceType::BRANCH, reference: '<REFERENCE>', activate: false # optional ) diff --git a/docs/examples/1.8.x/server-ruby/examples/sites/create-vcs-deployment.md b/docs/examples/1.8.x/server-ruby/examples/sites/create-vcs-deployment.md index 2e72b6e3f1..e0a6ff3af0 100644 --- a/docs/examples/1.8.x/server-ruby/examples/sites/create-vcs-deployment.md +++ b/docs/examples/1.8.x/server-ruby/examples/sites/create-vcs-deployment.md @@ -12,7 +12,7 @@ sites = Sites.new(client) result = sites.create_vcs_deployment( site_id: '<SITE_ID>', - type: VCSDeploymentType::BRANCH, + type: VCSReferenceType::BRANCH, reference: '<REFERENCE>', activate: false # optional ) diff --git a/src/Appwrite/Auth/Auth.php b/src/Appwrite/Auth/Auth.php deleted file mode 100644 index 9af5045fa4..0000000000 --- a/src/Appwrite/Auth/Auth.php +++ /dev/null @@ -1,515 +0,0 @@ -<?php - -namespace Appwrite\Auth; - -use Appwrite\Auth\Hash\Argon2; -use Appwrite\Auth\Hash\Bcrypt; -use Appwrite\Auth\Hash\Md5; -use Appwrite\Auth\Hash\Phpass; -use Appwrite\Auth\Hash\Scrypt; -use Appwrite\Auth\Hash\Scryptmodified; -use Appwrite\Auth\Hash\Sha; -use Utopia\Database\DateTime; -use Utopia\Database\Document; -use Utopia\Database\Helpers\Role; -use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\Roles; - -class Auth -{ - public const SUPPORTED_ALGOS = [ - 'argon2', - 'bcrypt', - 'md5', - 'sha', - 'phpass', - 'scrypt', - 'scryptMod', - 'plaintext' - ]; - - public const DEFAULT_ALGO = 'argon2'; - public const DEFAULT_ALGO_OPTIONS = ['type' => 'argon2', 'memoryCost' => 2048, 'timeCost' => 4, 'threads' => 3]; - - /** - * User Roles. - */ - public const USER_ROLE_ANY = 'any'; - public const USER_ROLE_GUESTS = 'guests'; - public const USER_ROLE_USERS = 'users'; - public const USER_ROLE_ADMIN = 'admin'; - public const USER_ROLE_DEVELOPER = 'developer'; - public const USER_ROLE_OWNER = 'owner'; - public const USER_ROLE_APPS = 'apps'; - public const USER_ROLE_SYSTEM = 'system'; - - /** - * Activity associated with user or the app. - */ - public const ACTIVITY_TYPE_APP = 'app'; - public const ACTIVITY_TYPE_USER = 'user'; - public const ACTIVITY_TYPE_GUEST = 'guest'; - - /** - * Token Types. - */ - public const TOKEN_TYPE_LOGIN = 1; // Deprecated - public const TOKEN_TYPE_VERIFICATION = 2; - public const TOKEN_TYPE_RECOVERY = 3; - public const TOKEN_TYPE_INVITE = 4; - public const TOKEN_TYPE_MAGIC_URL = 5; - public const TOKEN_TYPE_PHONE = 6; - public const TOKEN_TYPE_OAUTH2 = 7; - public const TOKEN_TYPE_GENERIC = 8; - public const TOKEN_TYPE_EMAIL = 9; // OTP - - /** - * Session Providers. - */ - public const SESSION_PROVIDER_EMAIL = 'email'; - public const SESSION_PROVIDER_ANONYMOUS = 'anonymous'; - public const SESSION_PROVIDER_MAGIC_URL = 'magic-url'; - public const SESSION_PROVIDER_PHONE = 'phone'; - public const SESSION_PROVIDER_OAUTH2 = 'oauth2'; - public const SESSION_PROVIDER_TOKEN = 'token'; - public const SESSION_PROVIDER_SERVER = 'server'; - - /** - * Token Expiration times. - */ - public const TOKEN_EXPIRATION_LOGIN_LONG = 31536000; /* 1 year */ - public const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */ - public const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */ - public const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */ - public const TOKEN_EXPIRATION_OTP = 60 * 15; /* 15 minutes */ - public const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */ - - /** - * Token Lengths. - */ - public const TOKEN_LENGTH_MAGIC_URL = 64; - public const TOKEN_LENGTH_VERIFICATION = 256; - public const TOKEN_LENGTH_RECOVERY = 256; - public const TOKEN_LENGTH_OAUTH2 = 64; - public const TOKEN_LENGTH_SESSION = 256; - - /** - * MFA - */ - public const MFA_RECENT_DURATION = 1800; // 30 mins - - /** - * @var string - */ - public static $cookieName = 'a_session'; - - /** - * @var string - */ - public static $cookieNamePreview = 'a_jwt_console'; - - /** - * User Unique ID. - * - * @var string - */ - public static $unique = ''; - - /** - * User Secret Key. - * - * @var string - */ - public static $secret = ''; - - /** - * Set Cookie Name. - * - * @param $string - * - * @return string - */ - public static function setCookieName($string) - { - return self::$cookieName = $string; - } - - /** - * Encode Session. - * - * @param string $id - * @param string $secret - * - * @return string - */ - public static function encodeSession($id, $secret) - { - return \base64_encode(\json_encode([ - 'id' => $id, - 'secret' => $secret, - ])); - } - - /** - * Token type to session provider mapping. - */ - public static function getSessionProviderByTokenType(int $type): string - { - switch ($type) { - case Auth::TOKEN_TYPE_VERIFICATION: - case Auth::TOKEN_TYPE_RECOVERY: - case Auth::TOKEN_TYPE_INVITE: - return Auth::SESSION_PROVIDER_EMAIL; - case Auth::TOKEN_TYPE_MAGIC_URL: - return Auth::SESSION_PROVIDER_MAGIC_URL; - case Auth::TOKEN_TYPE_PHONE: - return Auth::SESSION_PROVIDER_PHONE; - case Auth::TOKEN_TYPE_OAUTH2: - return Auth::SESSION_PROVIDER_OAUTH2; - default: - return Auth::SESSION_PROVIDER_TOKEN; - } - } - - /** - * Decode Session. - * - * @param string $session - * - * @return array - * - * @throws \Exception - */ - public static function decodeSession($session) - { - $session = \json_decode(\base64_decode($session), true); - $default = ['id' => null, 'secret' => '']; - - if (!\is_array($session)) { - return $default; - } - - return \array_merge($default, $session); - } - - /** - * Encode. - * - * One-way encryption - * - * @param $string - * - * @return string - */ - public static function hash(string $string) - { - return \hash('sha256', $string); - } - - /** - * Password Hash. - * - * One way string hashing for user passwords - * - * @param string $string - * @param string $algo hashing algorithm to use - * @param array $options algo-specific options - * - * @return bool|string|null - */ - public static function passwordHash(string $string, string $algo, array $options = []) - { - // Plain text not supported, just an alias. Switch to recommended algo - if ($algo === 'plaintext') { - $algo = Auth::DEFAULT_ALGO; - $options = Auth::DEFAULT_ALGO_OPTIONS; - } - - if (!\in_array($algo, Auth::SUPPORTED_ALGOS)) { - throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.'); - } - - switch ($algo) { - case 'argon2': - $hasher = new Argon2($options); - return $hasher->hash($string); - case 'bcrypt': - $hasher = new Bcrypt($options); - return $hasher->hash($string); - case 'md5': - $hasher = new Md5($options); - return $hasher->hash($string); - case 'sha': - $hasher = new Sha($options); - return $hasher->hash($string); - case 'phpass': - $hasher = new Phpass($options); - return $hasher->hash($string); - case 'scrypt': - $hasher = new Scrypt($options); - return $hasher->hash($string); - case 'scryptMod': - $hasher = new Scryptmodified($options); - return $hasher->hash($string); - default: - throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.'); - } - } - - /** - * Password verify. - * - * @param string $plain - * @param string $hash - * @param string $algo hashing algorithm used to hash - * @param array $options algo-specific options - * - * @return bool - */ - public static function passwordVerify(string $plain, string $hash, string $algo, array $options = []) - { - // Plain text not supported, just an alias. Switch to recommended algo - if ($algo === 'plaintext') { - $algo = Auth::DEFAULT_ALGO; - $options = Auth::DEFAULT_ALGO_OPTIONS; - } - - if (!\in_array($algo, Auth::SUPPORTED_ALGOS)) { - throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.'); - } - - switch ($algo) { - case 'argon2': - $hasher = new Argon2($options); - return $hasher->verify($plain, $hash); - case 'bcrypt': - $hasher = new Bcrypt($options); - return $hasher->verify($plain, $hash); - case 'md5': - $hasher = new Md5($options); - return $hasher->verify($plain, $hash); - case 'sha': - $hasher = new Sha($options); - return $hasher->verify($plain, $hash); - case 'phpass': - $hasher = new Phpass($options); - return $hasher->verify($plain, $hash); - case 'scrypt': - $hasher = new Scrypt($options); - return $hasher->verify($plain, $hash); - case 'scryptMod': - $hasher = new Scryptmodified($options); - return $hasher->verify($plain, $hash); - default: - throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.'); - } - } - - /** - * Password Generator. - * - * Generate random password string - * - * @param int $length - * - * @return string - */ - public static function passwordGenerator(int $length = 20): string - { - return \bin2hex(\random_bytes($length)); - } - - /** - * Token Generator. - * - * Generate random password string - * - * @param int $length Length of returned token - * - * @return string - */ - public static function tokenGenerator(int $length = 256): string - { - if ($length <= 0) { - throw new \Exception('Token length must be greater than 0'); - } - - $bytesLength = (int) ceil($length / 2); - $token = \bin2hex(\random_bytes($bytesLength)); - - return substr($token, 0, $length); - } - - /** - * Code Generator. - * - * Generate random code string - * - * @param int $length - * - * @return string - */ - public static function codeGenerator(int $length = 6): string - { - $value = ''; - - for ($i = 0; $i < $length; $i++) { - $value .= random_int(0, 9); - } - - return $value; - } - - /** - * Verify token and check that its not expired. - * - * @param array<Document> $tokens - * @param int $type Type of token to verify, if null will verify any type - * @param string $secret - * - * @return false|Document - */ - public static function tokenVerify(array $tokens, int $type = null, string $secret): false|Document - { - foreach ($tokens as $token) { - if ( - $token->isSet('secret') && - $token->isSet('expire') && - $token->isSet('type') && - ($type === null || $token->getAttribute('type') === $type) && - $token->getAttribute('secret') === self::hash($secret) && - DateTime::formatTz($token->getAttribute('expire')) >= DateTime::formatTz(DateTime::now()) - ) { - return $token; - } - } - - return false; - } - - /** - * Verify session and check that its not expired. - * - * @param array<Document> $sessions - * @param string $secret - * - * @return bool|string - */ - public static function sessionVerify(array $sessions, string $secret) - { - foreach ($sessions as $session) { - if ( - $session->isSet('secret') && - $session->isSet('provider') && - $session->getAttribute('secret') === self::hash($secret) && - DateTime::formatTz(DateTime::format(new \DateTime($session->getAttribute('expire')))) >= DateTime::formatTz(DateTime::now()) - ) { - return $session->getId(); - } - } - - return false; - } - - /** - * Is Privileged User? - * - * @param array<string> $roles - * - * @return bool - */ - public static function isPrivilegedUser(array $roles): bool - { - if ( - in_array(self::USER_ROLE_OWNER, $roles) || - in_array(self::USER_ROLE_DEVELOPER, $roles) || - in_array(self::USER_ROLE_ADMIN, $roles) - ) { - return true; - } - - return false; - } - - /** - * Is App User? - * - * @param array<string> $roles - * - * @return bool - */ - public static function isAppUser(array $roles): bool - { - if (in_array(self::USER_ROLE_APPS, $roles)) { - return true; - } - - return false; - } - - /** - * Returns all roles for a user. - * - * @param Document $user - * @return array<string> - */ - public static function getRoles(Document $user): array - { - $roles = []; - - if (!self::isPrivilegedUser(Authorization::getRoles()) && !self::isAppUser(Authorization::getRoles())) { - if ($user->getId()) { - $roles[] = Role::user($user->getId())->toString(); - $roles[] = Role::users()->toString(); - - $emailVerified = $user->getAttribute('emailVerification', false); - $phoneVerified = $user->getAttribute('phoneVerification', false); - - if ($emailVerified || $phoneVerified) { - $roles[] = Role::user($user->getId(), Roles::DIMENSION_VERIFIED)->toString(); - $roles[] = Role::users(Roles::DIMENSION_VERIFIED)->toString(); - } else { - $roles[] = Role::user($user->getId(), Roles::DIMENSION_UNVERIFIED)->toString(); - $roles[] = Role::users(Roles::DIMENSION_UNVERIFIED)->toString(); - } - } else { - return [Role::guests()->toString()]; - } - } - - foreach ($user->getAttribute('memberships', []) as $node) { - if (!isset($node['confirm']) || !$node['confirm']) { - continue; - } - - if (isset($node['$id']) && isset($node['teamId'])) { - $roles[] = Role::team($node['teamId'])->toString(); - $roles[] = Role::member($node['$id'])->toString(); - - if (isset($node['roles'])) { - foreach ($node['roles'] as $nodeRole) { // Set all team roles - $roles[] = Role::team($node['teamId'], $nodeRole)->toString(); - } - } - } - } - - foreach ($user->getAttribute('labels', []) as $label) { - $roles[] = 'label:' . $label; - } - - return $roles; - } - - /** - * Check if user is anonymous. - * - * @param Document $user - * @return bool - */ - public static function isAnonymousUser(Document $user): bool - { - return is_null($user->getAttribute('email')) - && is_null($user->getAttribute('phone')); - } -} diff --git a/src/Appwrite/Auth/Hash.php b/src/Appwrite/Auth/Hash.php deleted file mode 100644 index 7134057581..0000000000 --- a/src/Appwrite/Auth/Hash.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php - -namespace Appwrite\Auth; - -abstract class Hash -{ - /** - * @var array $options Hashing-algo specific options - */ - protected array $options = []; - - /** - * @param array $options Hashing-algo specific options - */ - public function __construct(array $options = []) - { - $this->setOptions($options); - } - - /** - * Set hashing algo options - * - * @param array $options Hashing-algo specific options - */ - public function setOptions(array $options): self - { - $this->options = \array_merge([], $this->getDefaultOptions(), $options); - return $this; - } - - /** - * Get hashing algo options - * - * @return array $options Hashing-algo specific options - */ - public function getOptions(): array - { - return $this->options; - } - - /** - * @param string $password Input password to hash - * - * @return string hash - */ - abstract public function hash(string $password): string; - - /** - * @param string $password Input password to validate - * @param string $hash Hash to verify password against - * - * @return boolean true if password matches hash - */ - abstract public function verify(string $password, string $hash): bool; - - /** - * Get default options for specific hashing algo - * - * @return array options named array - */ - abstract public function getDefaultOptions(): array; -} diff --git a/src/Appwrite/Auth/Hash/Argon2.php b/src/Appwrite/Auth/Hash/Argon2.php deleted file mode 100644 index c723b077b1..0000000000 --- a/src/Appwrite/Auth/Hash/Argon2.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php - -namespace Appwrite\Auth\Hash; - -use Appwrite\Auth\Hash; - -/* - * Argon2 accepted options: - * int threads - * int time_cost - * int memory_cost - * - * Reference: https://www.php.net/manual/en/function.password-hash.php#example-983 -*/ -class Argon2 extends Hash -{ - /** - * @param string $password Input password to hash - * - * @return string hash - */ - public function hash(string $password): string - { - return \password_hash($password, PASSWORD_ARGON2ID, $this->getOptions()); - } - - /** - * @param string $password Input password to validate - * @param string $hash Hash to verify password against - * - * @return boolean true if password matches hash - */ - public function verify(string $password, string $hash): bool - { - return \password_verify($password, $hash); - } - - /** - * Get default options for specific hashing algo - * - * @return array options named array - */ - public function getDefaultOptions(): array - { - return ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3]; - } -} diff --git a/src/Appwrite/Auth/Hash/Bcrypt.php b/src/Appwrite/Auth/Hash/Bcrypt.php deleted file mode 100644 index 8b6177f33a..0000000000 --- a/src/Appwrite/Auth/Hash/Bcrypt.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php - -namespace Appwrite\Auth\Hash; - -use Appwrite\Auth\Hash; - -/* - * Bcrypt accepted options: - * int cost - * string? salt; auto-generated if empty - * - * Reference: https://www.php.net/manual/en/password.constants.php -*/ -class Bcrypt extends Hash -{ - /** - * @param string $password Input password to hash - * - * @return string hash - */ - public function hash(string $password): string - { - return \password_hash($password, PASSWORD_BCRYPT, $this->getOptions()); - } - - /** - * @param string $password Input password to validate - * @param string $hash Hash to verify password against - * - * @return boolean true if password matches hash - */ - public function verify(string $password, string $hash): bool - { - return \password_verify($password, $hash); - } - - /** - * Get default options for specific hashing algo - * - * @return array options named array - */ - public function getDefaultOptions(): array - { - return [ 'cost' => 8 ]; - } -} diff --git a/src/Appwrite/Auth/Hash/Md5.php b/src/Appwrite/Auth/Hash/Md5.php deleted file mode 100644 index 8ade3dd5e2..0000000000 --- a/src/Appwrite/Auth/Hash/Md5.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php - -namespace Appwrite\Auth\Hash; - -use Appwrite\Auth\Hash; - -/* - * MD5 does not accept any options. - * - * Reference: https://www.php.net/manual/en/function.md5.php -*/ -class Md5 extends Hash -{ - /** - * @param string $password Input password to hash - * - * @return string hash - */ - public function hash(string $password): string - { - return \md5($password); - } - - /** - * @param string $password Input password to validate - * @param string $hash Hash to verify password against - * - * @return boolean true if password matches hash - */ - public function verify(string $password, string $hash): bool - { - return $this->hash($password) === $hash; - } - - /** - * Get default options for specific hashing algo - * - * @return array options named array - */ - public function getDefaultOptions(): array - { - return []; - } -} diff --git a/src/Appwrite/Auth/Hash/Phpass.php b/src/Appwrite/Auth/Hash/Phpass.php deleted file mode 100644 index 988c38cc8d..0000000000 --- a/src/Appwrite/Auth/Hash/Phpass.php +++ /dev/null @@ -1,290 +0,0 @@ -<?php - -/** - * Portable PHP password hashing framework. - * source Version 0.5 / genuine. - * Written by Solar Designer <solar at openwall.com> in 2004-2017 and placed in - * the public domain. Revised in subsequent years, still public domain. - * There's absolutely no warranty. - * The homepage URL for the source framework is: http://www.openwall.com/phpass/ - * Please be sure to update the Version line if you edit this file in any way. - * It is suggested that you leave the main version number intact, but indicate - * your project name (after the slash) and add your own revision information. - * Please do not change the "private" password hashing method implemented in - * here, thereby making your hashes incompatible. However, if you must, please - * change the hash type identifier (the "$P$") to something different. - * Obviously, since this code is in the public domain, the above are not - * requirements (there can be none), but merely suggestions. - * - * @author Solar Designer <solar@openwall.com> - * @copyright Copyright (C) 2017 All rights reserved. - * @license http://www.opensource.org/licenses/mit-license.html MIT License; see LICENSE.txt - */ - -namespace Appwrite\Auth\Hash; - -use Appwrite\Auth\Hash; - -/* - * PHPass accepted options: - * int iteration_count_log2; The Logarithmic cost value used when generating hash values indicating the number of rounds used to generate hashes - * string portable_hashes - * string random_state; The cached random state - * - * Reference: https://github.com/photodude/phpass -*/ -class Phpass extends Hash -{ - /** - * Alphabet used in itoa64 conversions. - * - * @var string - * @since 0.1.0 - */ - protected string $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - - /** - * Get default options for specific hashing algo - * - * @return array options named array - */ - public function getDefaultOptions(): array - { - $randomState = \microtime(); - if (\function_exists('getmypid')) { - $randomState .= getmypid(); - } - - return ['iteration_count_log2' => 8, 'portable_hashes' => false, 'random_state' => $randomState]; - } - - /** - * @param string $password Input password to hash - * - * @return string hash - */ - public function hash(string $password): string - { - $options = $this->getDefaultOptions(); - - $random = ''; - if (CRYPT_BLOWFISH === 1 && !$options['portable_hashes']) { - $random = $this->getRandomBytes(16, $options); - $hash = crypt($password, $this->gensaltBlowfish($random, $options)); - if (strlen($hash) === 60) { - return $hash; - } - } - if (strlen($random) < 6) { - $random = $this->getRandomBytes(6, $options); - } - $hash = $this->cryptPrivate($password, $this->gensaltPrivate($random, $options)); - if (strlen($hash) === 34) { - return $hash; - } - - /** - * Returning '*' on error is safe here, but would _not_ be safe - * in a crypt(3)-like function used _both_ for generating new - * hashes and for validating passwords against existing hashes. - */ - return '*'; - } - - /** - * @param string $password Input password to validate - * @param string $hash Hash to verify password against - * - * @return boolean true if password matches hash - */ - public function verify(string $password, string $hash): bool - { - $verificationHash = $this->cryptPrivate($password, $hash); - if ($verificationHash[0] === '*') { - $verificationHash = crypt($password, $hash); - } - - /** - * This is not constant-time. In order to keep the code simple, - * for timing safety we currently rely on the salts being - * unpredictable, which they are at least in the non-fallback - * cases (that is, when we use /dev/urandom and bcrypt). - */ - return $hash === $verificationHash; - } - - /** - * @param int $count - * - * @return String $output - * @since 0.1.0 - * @throws Exception Thows an Exception if the $count parameter is not a positive integer. - */ - protected function getRandomBytes(int $count, array $options): string - { - if (!is_int($count) || $count < 1) { - throw new \Exception('Argument count must be a positive integer'); - } - $output = ''; - if (@is_readable('/dev/urandom') && ($fh = @fopen('/dev/urandom', 'rb'))) { - $output = fread($fh, $count); - fclose($fh); - } - - if (strlen($output) < $count) { - $output = ''; - - for ($i = 0; $i < $count; $i += 16) { - $options['iteration_count_log2'] = md5(microtime() . $options['iteration_count_log2']); - $output .= md5($options['iteration_count_log2'], true); - } - - $output = substr($output, 0, $count); - } - - return $output; - } - - /** - * @param String $input - * @param int $count - * - * @return String $output - * @since 0.1.0 - * @throws Exception Thows an Exception if the $count parameter is not a positive integer. - */ - protected function encode64($input, $count) - { - if (!is_int($count) || $count < 1) { - throw new \Exception('Argument count must be a positive integer'); - } - $output = ''; - $i = 0; - do { - $value = ord($input[$i++]); - $output .= $this->itoa64[$value & 0x3f]; - if ($i < $count) { - $value |= ord($input[$i]) << 8; - } - $output .= $this->itoa64[($value >> 6) & 0x3f]; - if ($i++ >= $count) { - break; - } - if ($i < $count) { - $value |= ord($input[$i]) << 16; - } - $output .= $this->itoa64[($value >> 12) & 0x3f]; - if ($i++ >= $count) { - break; - } - $output .= $this->itoa64[($value >> 18) & 0x3f]; - } while ($i < $count); - - return $output; - } - - /** - * @param String $input - * - * @return String $output - * @since 0.1.0 - */ - private function gensaltPrivate($input, $options) - { - $output = '$P$'; - $output .= $this->itoa64[min($options['iteration_count_log2'] + ((PHP_VERSION >= '5') ? 5 : 3), 30)]; - $output .= $this->encode64($input, 6); - - return $output; - } - - /** - * @param String $password - * @param String $setting - * - * @return String $output - * @since 0.1.0 - */ - private function cryptPrivate($password, $setting) - { - $output = '*0'; - if (substr($setting, 0, 2) === $output) { - $output = '*1'; - } - $id = substr($setting, 0, 3); - // We use "$P$", phpBB3 uses "$H$" for the same thing - if ($id !== '$P$' && $id !== '$H$') { - return $output; - } - $count_log2 = strpos($this->itoa64, $setting[3]); - if ($count_log2 < 7 || $count_log2 > 30) { - return $output; - } - $count = 1 << $count_log2; - $salt = substr($setting, 4, 8); - if (strlen($salt) !== 8) { - return $output; - } - /** - * We were kind of forced to use MD5 here since it's the only - * cryptographic primitive that was available in all versions of PHP - * in use. To implement our own low-level crypto in PHP - * would have result in much worse performance and - * consequently in lower iteration counts and hashes that are - * quicker to crack (by non-PHP code). - */ - $hash = md5($salt . $password, true); - do { - $hash = md5($hash . $password, true); - } while (--$count); - $output = substr($setting, 0, 12); - $output .= $this->encode64($hash, 16); - - return $output; - } - - /** - * @param String $input - * - * @return String $output - * @since 0.1.0 - */ - private function gensaltBlowfish($input, $options) - { - /** - * This one needs to use a different order of characters and a - * different encoding scheme from the one in encode64() above. - * We care because the last character in our encoded string will - * only represent 2 bits. While two known implementations of - * bcrypt will happily accept and correct a salt string which - * has the 4 unused bits set to non-zero, we do not want to take - * chances and we also do not want to waste an additional byte - * of entropy. - */ - $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - $output = '$2a$'; - $output .= chr(ord('0') + intval($options['iteration_count_log2'] / 10)); - $output .= chr(ord('0') + $options['iteration_count_log2'] % 10); - $output .= '$'; - $i = 0; - do { - $c1 = ord($input[$i++]); - $output .= $itoa64[$c1 >> 2]; - $c1 = ($c1 & 0x03) << 4; - if ($i >= 16) { - $output .= $itoa64[$c1]; - break; - } - $c2 = ord($input[$i++]); - $c1 |= $c2 >> 4; - $output .= $itoa64[$c1]; - $c1 = ($c2 & 0x0f) << 2; - $c2 = ord($input[$i++]); - $c1 |= $c2 >> 6; - $output .= $itoa64[$c1]; - $output .= $itoa64[$c2 & 0x3f]; - } while (1); - - return $output; - } -} diff --git a/src/Appwrite/Auth/Hash/Scrypt.php b/src/Appwrite/Auth/Hash/Scrypt.php deleted file mode 100644 index 821b1fba69..0000000000 --- a/src/Appwrite/Auth/Hash/Scrypt.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php - -namespace Appwrite\Auth\Hash; - -use Appwrite\Auth\Hash; - -/* - * Scrypt accepted options: - * string? salt; auto-generated if empty - * int costCpu - * int costMemory - * int costParallel - * int length - * - * Reference: https://github.com/DomBlack/php-scrypt/blob/master/scrypt.php#L112-L116 -*/ -class Scrypt extends Hash -{ - /** - * @param string $password Input password to hash - * - * @return string hash - */ - public function hash(string $password): string - { - $options = $this->getOptions(); - - return \scrypt($password, $options['salt'], $options['costCpu'], $options['costMemory'], $options['costParallel'], $options['length']); - } - - /** - * @param string $password Input password to validate - * @param string $hash Hash to verify password against - * - * @return boolean true if password matches hash - */ - public function verify(string $password, string $hash): bool - { - return $hash === $this->hash($password); - } - - /** - * Get default options for specific hashing algo - * - * @return array options named array - */ - public function getDefaultOptions(): array - { - return [ 'costCpu' => 8, 'costMemory' => 14, 'costParallel' => 1, 'length' => 64 ]; - } -} diff --git a/src/Appwrite/Auth/Hash/Scryptmodified.php b/src/Appwrite/Auth/Hash/Scryptmodified.php deleted file mode 100644 index 7717f324e5..0000000000 --- a/src/Appwrite/Auth/Hash/Scryptmodified.php +++ /dev/null @@ -1,80 +0,0 @@ -<?php - -namespace Appwrite\Auth\Hash; - -use Appwrite\Auth\Hash; - -/* - * This is Scrypt hash with some additional steps added by Google. - * - * string salt - * string saltSeparator - * strin signerKey - * - * Reference: https://github.com/DomBlack/php-scrypt/blob/master/scrypt.php#L112-L116 -*/ -class Scryptmodified extends Hash -{ - /** - * @param string $password Input password to hash - * - * @return string hash - */ - public function hash(string $password): string - { - $options = $this->getOptions(); - - $derivedKeyBytes = $this->generateDerivedKey($password); - $signerKeyBytes = \base64_decode($options['signerKey']); - - $hashedPassword = $this->hashKeys($signerKeyBytes, $derivedKeyBytes); - - return \base64_encode($hashedPassword); - } - - /** - * @param string $password Input password to validate - * @param string $hash Hash to verify password against - * - * @return boolean true if password matches hash - */ - public function verify(string $password, string $hash): bool - { - return $this->hash($password) === $hash; - } - - /** - * Get default options for specific hashing algo - * - * @return array options named array - */ - public function getDefaultOptions(): array - { - return [ ]; - } - - private function generateDerivedKey(string $password) - { - $options = $this->getOptions(); - - $saltBytes = \base64_decode($options['salt']); - $saltSeparatorBytes = \base64_decode($options['saltSeparator']); - - $password = mb_convert_encoding($password, 'UTF-8'); - $derivedKey = \scrypt($password, $saltBytes . $saltSeparatorBytes, 16384, 8, 1, 64); - $derivedKey = \hex2bin($derivedKey); - - return $derivedKey; - } - - private function hashKeys($signerKeyBytes, $derivedKeyBytes): string - { - $key = \substr($derivedKeyBytes, 0, 32); - - $iv = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; - - $hash = \openssl_encrypt($signerKeyBytes, 'aes-256-ctr', $key, OPENSSL_RAW_DATA, $iv); - - return $hash; - } -} diff --git a/src/Appwrite/Auth/Hash/Sha.php b/src/Appwrite/Auth/Hash/Sha.php deleted file mode 100644 index c2ae3b52c1..0000000000 --- a/src/Appwrite/Auth/Hash/Sha.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php - -namespace Appwrite\Auth\Hash; - -use Appwrite\Auth\Hash; - -/* - * SHA accepted options: - * string? version. Allowed: - * - Version 1: sha1 - * - Version 2: sha224, sha256, sha384, sha512/224, sha512/256, sha512 - * - Version 3: sha3-224, sha3-256, sha3-384, sha3-512 - * - * Reference: https://www.php.net/manual/en/function.hash-algos.php -*/ -class Sha extends Hash -{ - /** - * @param string $password Input password to hash - * - * @return string hash - */ - public function hash(string $password): string - { - $algo = $this->getOptions()['version']; - - return \hash($algo, $password); - } - - /** - * @param string $password Input password to validate - * @param string $hash Hash to verify password against - * - * @return boolean true if password matches hash - */ - public function verify(string $password, string $hash): bool - { - return $this->hash($password) === $hash; - } - - /** - * Get default options for specific hashing algo - * - * @return array options named array - */ - public function getDefaultOptions(): array - { - return [ 'version' => 'sha3-512' ]; - } -} diff --git a/src/Appwrite/Auth/Key.php b/src/Appwrite/Auth/Key.php index 44a75a6ee3..b1f3836fb6 100644 --- a/src/Appwrite/Auth/Key.php +++ b/src/Appwrite/Auth/Key.php @@ -5,6 +5,7 @@ namespace Appwrite\Auth; use Ahc\Jwt\JWT; use Ahc\Jwt\JWTException; use Appwrite\Extend\Exception; +use Appwrite\Utopia\Database\Documents\User; use Utopia\Config\Config; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -110,16 +111,16 @@ class Key $secret = $key; } - $role = Auth::USER_ROLE_APPS; + $role = User::ROLE_APPS; $roles = Config::getParam('roles', []); - $scopes = $roles[Auth::USER_ROLE_APPS]['scopes'] ?? []; + $scopes = $roles[User::ROLE_APPS]['scopes'] ?? []; $expired = false; $guestKey = new Key( $project->getId(), $type, - Auth::USER_ROLE_GUESTS, - $roles[Auth::USER_ROLE_GUESTS]['scopes'] ?? [], + User::ROLE_GUESTS, + $roles[User::ROLE_GUESTS]['scopes'] ?? [], 'UNKNOWN' ); diff --git a/src/Appwrite/Auth/MFA/Type.php b/src/Appwrite/Auth/MFA/Type.php index 3516ec3780..d1e267965a 100644 --- a/src/Appwrite/Auth/MFA/Type.php +++ b/src/Appwrite/Auth/MFA/Type.php @@ -2,8 +2,8 @@ namespace Appwrite\Auth\MFA; -use Appwrite\Auth\Auth; use OTPHP\OTP; +use Utopia\Auth\Proofs\Token; abstract class Type { @@ -51,9 +51,10 @@ abstract class Type public static function generateBackupCodes(int $length = 10, int $total = 6): array { $backups = []; + $token = new Token($length); for ($i = 0; $i < $total; $i++) { - $backups[] = Auth::tokenGenerator($length); + $backups[] = $token->generate(); } return $backups; diff --git a/src/Appwrite/Auth/Validator/PasswordHistory.php b/src/Appwrite/Auth/Validator/PasswordHistory.php index f623ca180d..9b40b6a794 100644 --- a/src/Appwrite/Auth/Validator/PasswordHistory.php +++ b/src/Appwrite/Auth/Validator/PasswordHistory.php @@ -2,7 +2,7 @@ namespace Appwrite\Auth\Validator; -use Appwrite\Auth\Auth; +use Utopia\Auth\Hash; /** * Password. @@ -12,16 +12,14 @@ use Appwrite\Auth\Auth; class PasswordHistory extends Password { protected array $history; - protected string $algo; - protected array $algoOptions; + protected Hash $hash; - public function __construct(array $history, string $algo, array $algoOptions = []) + public function __construct(array $history, Hash $hash) { parent::__construct(); $this->history = $history; - $this->algo = $algo; - $this->algoOptions = $algoOptions; + $this->hash = $hash; } /** @@ -46,7 +44,7 @@ class PasswordHistory extends Password public function isValid($value): bool { foreach ($this->history as $hash) { - if (!empty($hash) && Auth::passwordVerify($value, $hash, $this->algo, $this->algoOptions)) { + if (!empty($hash) && $this->hash->verify($value, $hash)) { return false; } } diff --git a/src/Appwrite/Migration/Version/V16.php b/src/Appwrite/Migration/Version/V16.php index 9d72af9563..061ace31d7 100644 --- a/src/Appwrite/Migration/Version/V16.php +++ b/src/Appwrite/Migration/Version/V16.php @@ -2,7 +2,6 @@ namespace Appwrite\Migration\Version; -use Appwrite\Auth\Auth; use Appwrite\Migration\Migration; use Utopia\CLI\Console; use Utopia\Config\Config; @@ -118,7 +117,7 @@ class V16 extends Migration * Set default authDuration */ $document->setAttribute('auths', array_merge($document->getAttribute('auths', []), [ - 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG + 'duration' => TOKEN_EXPIRATION_LOGIN_LONG ])); /** diff --git a/src/Appwrite/Migration/Version/V17.php b/src/Appwrite/Migration/Version/V17.php index fbbd4bfde0..79e2a8377d 100644 --- a/src/Appwrite/Migration/Version/V17.php +++ b/src/Appwrite/Migration/Version/V17.php @@ -2,8 +2,8 @@ namespace Appwrite\Migration\Version; -use Appwrite\Auth\Auth; use Appwrite\Migration\Migration; +use Utopia\Auth\Proofs\Password; use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\Document; @@ -270,7 +270,7 @@ class V17 extends Migration * Set hashOptions type */ $document->setAttribute('hashOptions', array_merge($document->getAttribute('hashOptions', []), [ - 'type' => $document->getAttribute('hash', Auth::DEFAULT_ALGO) + 'type' => $document->getAttribute('hash', (new Password())->getHash()->getName()) ])); break; } diff --git a/src/Appwrite/Migration/Version/V20.php b/src/Appwrite/Migration/Version/V20.php index 9ff041eb33..10e2706d0e 100644 --- a/src/Appwrite/Migration/Version/V20.php +++ b/src/Appwrite/Migration/Version/V20.php @@ -2,7 +2,6 @@ namespace Appwrite\Migration\Version; -use Appwrite\Auth\Auth; use Appwrite\Migration\Migration; use Exception; use PDOException; @@ -632,15 +631,15 @@ class V20 extends Migration } break; case 'sessions': - $duration = $this->project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $duration = $this->project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; $expire = DateTime::addSeconds(new \DateTime(), $duration); $document->setAttribute('expire', $expire); $factors = match ($document->getAttribute('provider')) { - Auth::SESSION_PROVIDER_EMAIL => ['password'], - Auth::SESSION_PROVIDER_PHONE => ['phone'], - Auth::SESSION_PROVIDER_ANONYMOUS => ['anonymous'], - Auth::SESSION_PROVIDER_TOKEN => ['token'], + SESSION_PROVIDER_EMAIL => ['password'], + SESSION_PROVIDER_PHONE => ['phone'], + SESSION_PROVIDER_ANONYMOUS => ['anonymous'], + SESSION_PROVIDER_TOKEN => ['token'], default => ['email'], }; diff --git a/src/Appwrite/Platform/Appwrite.php b/src/Appwrite/Platform/Appwrite.php index a77f894be7..4aa135c4f1 100644 --- a/src/Appwrite/Platform/Appwrite.php +++ b/src/Appwrite/Platform/Appwrite.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform; +use Appwrite\Platform\Modules\Account; use Appwrite\Platform\Modules\Console; use Appwrite\Platform\Modules\Core; use Appwrite\Platform\Modules\Databases; @@ -17,6 +18,7 @@ class Appwrite extends Platform public function __construct() { parent::__construct(new Core()); + $this->addModule(new Account\Module()); $this->addModule(new Databases\Module()); $this->addModule(new Projects\Module()); $this->addModule(new Functions\Module()); diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Create.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Create.php new file mode 100644 index 0000000000..2d83599964 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Create.php @@ -0,0 +1,141 @@ +<?php + +namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\Authenticators; + +use Appwrite\Auth\MFA\Type; +use Appwrite\Auth\MFA\Type\TOTP; +use Appwrite\Event\Event; +use Appwrite\Extend\Exception; +use Appwrite\SDK\AuthType; +use Appwrite\SDK\ContentType; +use Appwrite\SDK\Deprecated; +use Appwrite\SDK\Method; +use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Response; +use Utopia\Database\Database; +use Utopia\Database\Document; +use Utopia\Database\Helpers\ID; +use Utopia\Database\Helpers\Permission; +use Utopia\Database\Helpers\Role; +use Utopia\Platform\Action; +use Utopia\Platform\Scope\HTTP; +use Utopia\Validator\WhiteList; + +class Create extends Action +{ + use HTTP; + + public static function getName(): string + { + return 'createMFAAuthenticator'; + } + + public function __construct() + { + $this + ->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/account/mfa/authenticators/:type') + ->desc('Create authenticator') + ->groups(['api', 'account']) + ->label('event', 'users.[userId].update.mfa') + ->label('scope', 'account') + ->label('audits.event', 'user.update') + ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') + ->label('sdk', [ + new Method( + namespace: 'account', + group: 'mfa', + name: 'createMfaAuthenticator', + description: '/docs/references/account/create-mfa-authenticator.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_MFA_TYPE, + ) + ], + contentType: ContentType::JSON, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'account.createMFAAuthenticator', + ), + ), + new Method( + namespace: 'account', + group: 'mfa', + name: 'createMFAAuthenticator', + description: '/docs/references/account/create-mfa-authenticator.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_MFA_TYPE, + ) + ], + contentType: ContentType::JSON + ) + ]) + ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator. Must be `' . Type::TOTP . '`') + ->inject('response') + ->inject('project') + ->inject('user') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action( + string $type, + Response $response, + Document $project, + Document $user, + Database $dbForProject, + Event $queueForEvents + ): void { + $otp = (match ($type) { + Type::TOTP => new TOTP(), + default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Unknown type.') + }); + + $otp->setLabel($user->getAttribute('email')); + $otp->setIssuer($project->getAttribute('name')); + + $authenticator = TOTP::getAuthenticatorFromUser($user); + + if ($authenticator) { + if ($authenticator->getAttribute('verified')) { + throw new Exception(Exception::USER_AUTHENTICATOR_ALREADY_VERIFIED); + } + $dbForProject->deleteDocument('authenticators', $authenticator->getId()); + } + + $authenticator = new Document([ + '$id' => ID::unique(), + 'userId' => $user->getId(), + 'userInternalId' => $user->getSequence(), + 'type' => Type::TOTP, + 'verified' => false, + 'data' => [ + 'secret' => $otp->getSecret(), + ], + '$permissions' => [ + Permission::read(Role::user($user->getId())), + Permission::update(Role::user($user->getId())), + Permission::delete(Role::user($user->getId())), + ] + ]); + + $model = new Document([ + 'secret' => $otp->getSecret(), + 'uri' => $otp->getProvisioningUri() + ]); + + $authenticator = $dbForProject->createDocument('authenticators', $authenticator); + $dbForProject->purgeCachedDocument('users', $user->getId()); + + $queueForEvents->setParam('userId', $user->getId()); + + $response->dynamic($model, Response::MODEL_MFA_TYPE); + } +} diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Delete.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Delete.php new file mode 100644 index 0000000000..5c92bfff5c --- /dev/null +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Delete.php @@ -0,0 +1,107 @@ +<?php + +namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\Authenticators; + +use Appwrite\Auth\MFA\Type; +use Appwrite\Auth\MFA\Type\TOTP; +use Appwrite\Event\Event; +use Appwrite\Extend\Exception; +use Appwrite\SDK\AuthType; +use Appwrite\SDK\ContentType; +use Appwrite\SDK\Deprecated; +use Appwrite\SDK\Method; +use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Response; +use Utopia\Database\Database; +use Utopia\Database\Document; +use Utopia\Platform\Action; +use Utopia\Platform\Scope\HTTP; +use Utopia\Validator\WhiteList; + +class Delete extends Action +{ + use HTTP; + + public static function getName(): string + { + return 'deleteMFAAuthenticator'; + } + + public function __construct() + { + $this + ->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/account/mfa/authenticators/:type') + ->desc('Delete authenticator') + ->groups(['api', 'account', 'mfaProtected']) + ->label('event', 'users.[userId].delete.mfa') + ->label('scope', 'account') + ->label('audits.event', 'user.update') + ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') + ->label('sdk', [ + new Method( + namespace: 'account', + group: 'mfa', + name: 'deleteMfaAuthenticator', + description: '/docs/references/account/delete-mfa-authenticator.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_NOCONTENT, + model: Response::MODEL_NONE, + ) + ], + contentType: ContentType::NONE, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'account.deleteMFAAuthenticator', + ), + ), + new Method( + namespace: 'account', + group: 'mfa', + name: 'deleteMFAAuthenticator', + description: '/docs/references/account/delete-mfa-authenticator.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_NOCONTENT, + model: Response::MODEL_NONE, + ) + ], + contentType: ContentType::NONE + ) + ]) + ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.') + ->inject('response') + ->inject('user') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action( + string $type, + Response $response, + Document $user, + Database $dbForProject, + Event $queueForEvents + ): void { + $authenticator = (match ($type) { + Type::TOTP => TOTP::getAuthenticatorFromUser($user), + default => null + }); + + if (!$authenticator) { + throw new Exception(Exception::USER_AUTHENTICATOR_NOT_FOUND); + } + + $dbForProject->deleteDocument('authenticators', $authenticator->getId()); + $dbForProject->purgeCachedDocument('users', $user->getId()); + + $queueForEvents->setParam('userId', $user->getId()); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Update.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Update.php new file mode 100644 index 0000000000..b68a55c20b --- /dev/null +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Update.php @@ -0,0 +1,135 @@ +<?php + +namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\Authenticators; + +use Appwrite\Auth\MFA\Challenge; +use Appwrite\Auth\MFA\Type; +use Appwrite\Auth\MFA\Type\TOTP; +use Appwrite\Event\Event; +use Appwrite\Extend\Exception; +use Appwrite\SDK\AuthType; +use Appwrite\SDK\ContentType; +use Appwrite\SDK\Deprecated; +use Appwrite\SDK\Method; +use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Response; +use Utopia\Database\Database; +use Utopia\Database\Document; +use Utopia\Platform\Action; +use Utopia\Platform\Scope\HTTP; +use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; + +class Update extends Action +{ + use HTTP; + + public static function getName(): string + { + return 'updateMFAAuthenticator'; + } + + public function __construct() + { + $this + ->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT) + ->setHttpPath('/v1/account/mfa/authenticators/:type') + ->desc('Update authenticator (confirmation)') + ->groups(['api', 'account']) + ->label('event', 'users.[userId].update.mfa') + ->label('scope', 'account') + ->label('audits.event', 'user.update') + ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') + ->label('sdk', [ + new Method( + namespace: 'account', + group: 'mfa', + name: 'updateMfaAuthenticator', + description: '/docs/references/account/update-mfa-authenticator.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_USER, + ) + ], + contentType: ContentType::JSON, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'account.updateMFAAuthenticator', + ), + ), + new Method( + namespace: 'account', + group: 'mfa', + name: 'updateMFAAuthenticator', + description: '/docs/references/account/update-mfa-authenticator.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_USER, + ) + ], + contentType: ContentType::JSON + ) + ]) + ->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.') + ->param('otp', '', new Text(256), 'Valid verification token.') + ->inject('response') + ->inject('user') + ->inject('session') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action( + string $type, + string $otp, + Response $response, + Document $user, + Document $session, + Database $dbForProject, + Event $queueForEvents + ): void { + $authenticator = (match ($type) { + Type::TOTP => TOTP::getAuthenticatorFromUser($user), + default => null + }); + + if ($authenticator === null) { + throw new Exception(Exception::USER_AUTHENTICATOR_NOT_FOUND); + } + + if ($authenticator->getAttribute('verified')) { + throw new Exception(Exception::USER_AUTHENTICATOR_ALREADY_VERIFIED); + } + + $success = (match ($type) { + Type::TOTP => Challenge\TOTP::verify($user, $otp), + default => false + }); + + if (!$success) { + throw new Exception(Exception::USER_INVALID_TOKEN); + } + + $authenticator->setAttribute('verified', true); + + $dbForProject->updateDocument('authenticators', $authenticator->getId(), $authenticator); + $dbForProject->purgeCachedDocument('users', $user->getId()); + + $factors = $session->getAttribute('factors', []); + $factors[] = $type; + $factors = \array_values(\array_unique($factors)); + + $session->setAttribute('factors', $factors); + $dbForProject->updateDocument('sessions', $session->getId(), $session); + + $queueForEvents->setParam('userId', $user->getId()); + + $response->dynamic($user, Response::MODEL_ACCOUNT); + } +} diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php new file mode 100644 index 0000000000..10230df7af --- /dev/null +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php @@ -0,0 +1,337 @@ +<?php + +namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\Challenges; + +use Appwrite\Auth\MFA\Type; +use Appwrite\Detector\Detector; +use Appwrite\Event\Event; +use Appwrite\Event\Mail; +use Appwrite\Event\Messaging; +use Appwrite\Event\StatsUsage; +use Appwrite\Extend\Exception; +use Appwrite\SDK\ContentType; +use Appwrite\SDK\Deprecated; +use Appwrite\SDK\Method; +use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Template\Template; +use Appwrite\Utopia\Request; +use Appwrite\Utopia\Response; +use libphonenumber\PhoneNumberUtil; +use Utopia\Abuse\Abuse; +use Utopia\Auth\Proofs\Code as ProofsCode; +use Utopia\Auth\Proofs\Token as ProofsToken; +use Utopia\Database\Database; +use Utopia\Database\DateTime; +use Utopia\Database\Document; +use Utopia\Database\Helpers\Permission; +use Utopia\Database\Helpers\Role; +use Utopia\Locale\Locale; +use Utopia\Platform\Action; +use Utopia\Platform\Scope\HTTP; +use Utopia\Storage\Validator\FileName; +use Utopia\System\System; +use Utopia\Validator\WhiteList; + +class Create extends Action +{ + use HTTP; + + public static function getName(): string + { + return 'createMFAChallenge'; + } + + public function __construct() + { + $this + ->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/account/mfa/challenges') + ->httpAlias('/v1/account/mfa/challenge') + ->desc('Create MFA challenge') + ->groups(['api', 'account', 'mfa']) + ->label('scope', 'account') + ->label('event', 'users.[userId].challenges.[challengeId].create') + ->label('audits.event', 'challenge.create') + ->label('audits.resource', 'user/{response.userId}') + ->label('audits.userId', '{response.userId}') + ->label('sdk', [ + new Method( + namespace: 'account', + group: 'mfa', + name: 'createMfaChallenge', + description: '/docs/references/account/create-mfa-challenge.md', + auth: [], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_CREATED, + model: Response::MODEL_MFA_CHALLENGE, + ) + ], + contentType: ContentType::JSON, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'account.createMFAChallenge', + ), + ), + new Method( + namespace: 'account', + group: 'mfa', + name: 'createMFAChallenge', + description: '/docs/references/account/create-mfa-challenge.md', + auth: [], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_CREATED, + model: Response::MODEL_MFA_CHALLENGE, + ) + ], + contentType: ContentType::JSON + ) + ]) + ->label('abuse-limit', 10) + ->label('abuse-key', 'url:{url},userId:{userId}') + ->param('factor', '', new WhiteList([Type::EMAIL, Type::PHONE, Type::TOTP, Type::RECOVERY_CODE]), 'Factor used for verification. Must be one of following: `' . Type::EMAIL . '`, `' . Type::PHONE . '`, `' . Type::TOTP . '`, `' . Type::RECOVERY_CODE . '`.') + ->inject('response') + ->inject('dbForProject') + ->inject('user') + ->inject('locale') + ->inject('project') + ->inject('request') + ->inject('queueForEvents') + ->inject('queueForMessaging') + ->inject('queueForMails') + ->inject('timelimit') + ->inject('queueForStatsUsage') + ->inject('plan') + ->inject('proofForToken') + ->inject('proofForCode') + ->callback($this->action(...)); + } + + public function action( + string $factor, + Response $response, + Database $dbForProject, + Document $user, + Locale $locale, + Document $project, + Request $request, + Event $queueForEvents, + Messaging $queueForMessaging, + Mail $queueForMails, + callable $timelimit, + StatsUsage $queueForStatsUsage, + array $plan, + ProofsToken $proofForToken, + ProofsCode $proofForCode + ): void { + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_CONFIRM)); + + $code = $proofForCode->generate(); + $challenge = new Document([ + 'userId' => $user->getId(), + 'userInternalId' => $user->getSequence(), + 'type' => $factor, + 'token' => $proofForToken->generate(), + 'code' => $code, + 'expire' => $expire, + '$permissions' => [ + Permission::read(Role::user($user->getId())), + Permission::update(Role::user($user->getId())), + Permission::delete(Role::user($user->getId())), + ], + ]); + + $challenge = $dbForProject->createDocument('challenges', $challenge); + + // 9 levels up to project root + $templatesPath = \dirname(__DIR__, 9) . '/app/config/locale/templates'; + + switch ($factor) { + case Type::PHONE: + if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { + throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); + } + if (empty($user->getAttribute('phone'))) { + throw new Exception(Exception::USER_PHONE_NOT_FOUND); + } + if (!$user->getAttribute('phoneVerification')) { + throw new Exception(Exception::USER_PHONE_NOT_VERIFIED); + } + + $message = Template::fromFile($templatesPath . '/sms-base.tpl'); + + $customTemplate = $project->getAttribute('templates', [])['sms.mfaChallenge-' . $locale->default] ?? []; + if (!empty($customTemplate)) { + $message = $customTemplate['message'] ?? $message; + } + + $messageContent = Template::fromString($locale->getText("sms.verification.body")); + $messageContent + ->setParam('{{project}}', $project->getAttribute('name')) + ->setParam('{{secret}}', $code); + $messageContent = \strip_tags($messageContent->render()); + $message = $message->setParam('{{token}}', $messageContent); + + $message = $message->render(); + + $phone = $user->getAttribute('phone'); + $queueForMessaging + ->setType(MESSAGE_SEND_TYPE_INTERNAL) + ->setMessage(new Document([ + '$id' => $challenge->getId(), + 'data' => [ + 'content' => $code, + ], + ])) + ->setRecipients([$phone]) + ->setProviderType(MESSAGE_TYPE_SMS); + + if (isset($plan['authPhone'])) { + $timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days + $timelimit + ->setParam('{organizationId}', $project->getAttribute('teamId')); + + $abuse = new Abuse($timelimit); + if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') { + $helper = PhoneNumberUtil::getInstance(); + $countryCode = $helper->parse($phone)->getCountryCode(); + + if (!empty($countryCode)) { + $queueForStatsUsage + ->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1); + } + } + $queueForStatsUsage + ->addMetric(METRIC_AUTH_METHOD_PHONE, 1) + ->setProject($project) + ->trigger(); + } + break; + case Type::EMAIL: + if (empty(System::getEnv('_APP_SMTP_HOST'))) { + throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); + } + if (empty($user->getAttribute('email'))) { + throw new Exception(Exception::USER_EMAIL_NOT_FOUND); + } + if (!$user->getAttribute('emailVerification')) { + throw new Exception(Exception::USER_EMAIL_NOT_VERIFIED); + } + + $subject = $locale->getText("emails.mfaChallenge.subject"); + $preview = $locale->getText("emails.mfaChallenge.preview"); + $heading = $locale->getText("emails.mfaChallenge.heading"); + + $customTemplate = $project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ?? []; + $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); + + $validator = new FileName(); + if (!$validator->isValid($smtpBaseTemplate)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path'); + } + + $bodyTemplate = $templatesPath . '/' . $smtpBaseTemplate . '.tpl'; + + $detector = new Detector($request->getUserAgent('UNKNOWN')); + $agentOs = $detector->getOS(); + $agentClient = $detector->getClient(); + $agentDevice = $detector->getDevice(); + + $message = Template::fromFile($templatesPath . '/email-mfa-challenge.tpl'); + $message + ->setParam('{{hello}}', $locale->getText("emails.mfaChallenge.hello")) + ->setParam('{{description}}', $locale->getText("emails.mfaChallenge.description")) + ->setParam('{{clientInfo}}', $locale->getText("emails.mfaChallenge.clientInfo")) + ->setParam('{{thanks}}', $locale->getText("emails.mfaChallenge.thanks")) + ->setParam('{{signature}}', $locale->getText("emails.mfaChallenge.signature")); + + $body = $message->render(); + + $smtp = $project->getAttribute('smtp', []); + $smtpEnabled = $smtp['enabled'] ?? false; + + $senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); + $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); + $replyTo = ""; + + if ($smtpEnabled) { + if (!empty($smtp['senderEmail'])) { + $senderEmail = $smtp['senderEmail']; + } + if (!empty($smtp['senderName'])) { + $senderName = $smtp['senderName']; + } + if (!empty($smtp['replyTo'])) { + $replyTo = $smtp['replyTo']; + } + + $queueForMails + ->setSmtpHost($smtp['host'] ?? '') + ->setSmtpPort($smtp['port'] ?? '') + ->setSmtpUsername($smtp['username'] ?? '') + ->setSmtpPassword($smtp['password'] ?? '') + ->setSmtpSecure($smtp['secure'] ?? ''); + + if (!empty($customTemplate)) { + if (!empty($customTemplate['senderEmail'])) { + $senderEmail = $customTemplate['senderEmail']; + } + if (!empty($customTemplate['senderName'])) { + $senderName = $customTemplate['senderName']; + } + if (!empty($customTemplate['replyTo'])) { + $replyTo = $customTemplate['replyTo']; + } + + $body = $customTemplate['message'] ?? ''; + $subject = $customTemplate['subject'] ?? $subject; + } + + $queueForMails + ->setSmtpReplyTo($replyTo) + ->setSmtpSenderEmail($senderEmail) + ->setSmtpSenderName($senderName); + } + + $emailVariables = [ + 'heading' => $heading, + 'direction' => $locale->getText('settings.direction'), + 'user' => $user->getAttribute('name'), + 'project' => $project->getAttribute('name'), + 'otp' => $code, + 'agentDevice' => $agentDevice['deviceBrand'] ?? 'UNKNOWN', + 'agentClient' => $agentClient['clientName'] ?? 'UNKNOWN', + 'agentOs' => $agentOs['osName'] ?? 'UNKNOWN', + ]; + + if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) { + $emailVariables = array_merge($emailVariables, [ + 'accentColor' => APP_EMAIL_ACCENT_COLOR, + 'logoUrl' => APP_EMAIL_LOGO_URL, + 'twitterUrl' => APP_SOCIAL_TWITTER, + 'discordUrl' => APP_SOCIAL_DISCORD, + 'githubUrl' => APP_SOCIAL_GITHUB_APPWRITE, + 'termsUrl' => APP_EMAIL_TERMS_URL, + 'privacyUrl' => APP_EMAIL_PRIVACY_URL, + ]); + } + + $queueForMails + ->setSubject($subject) + ->setPreview($preview) + ->setBody($body) + ->setBodyTemplate($bodyTemplate) + ->setVariables($emailVariables) + ->setRecipient($user->getAttribute('email')) + ->trigger(); + break; + } + + $queueForEvents + ->setParam('userId', $user->getId()) + ->setParam('challengeId', $challenge->getId()); + + $response->dynamic($challenge, Response::MODEL_MFA_CHALLENGE); + } +} diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Update.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Update.php new file mode 100644 index 0000000000..40d17afa18 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Update.php @@ -0,0 +1,161 @@ +<?php + +namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\Challenges; + +use Appwrite\Auth\MFA\Challenge; +use Appwrite\Auth\MFA\Type; +use Appwrite\Event\Event; +use Appwrite\Extend\Exception; +use Appwrite\SDK\AuthType; +use Appwrite\SDK\ContentType; +use Appwrite\SDK\Deprecated; +use Appwrite\SDK\Method; +use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Response; +use Utopia\Database\Database; +use Utopia\Database\DateTime; +use Utopia\Database\Document; +use Utopia\Platform\Action; +use Utopia\Platform\Scope\HTTP; +use Utopia\Validator\Text; + +class Update extends Action +{ + use HTTP; + + public static function getName(): string + { + return 'updateMFAChallenge'; + } + + public function __construct() + { + $this + ->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT) + ->setHttpPath('/v1/account/mfa/challenges') + ->httpAlias('/v1/account/mfa/challenge') + ->desc('Update MFA challenge (confirmation)') + ->groups(['api', 'account', 'mfa']) + ->label('scope', 'account') + ->label('event', 'users.[userId].sessions.[sessionId].create') + ->label('audits.event', 'challenges.update') + ->label('audits.resource', 'user/{response.userId}') + ->label('audits.userId', '{response.userId}') + ->label('sdk', [ + new Method( + namespace: 'account', + group: 'mfa', + name: 'updateMfaChallenge', + description: '/docs/references/account/update-mfa-challenge.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_SESSION, + ) + ], + contentType: ContentType::JSON, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'account.updateMFAChallenge', + ), + ), + new Method( + namespace: 'account', + group: 'mfa', + name: 'updateMFAChallenge', + description: '/docs/references/account/update-mfa-challenge.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_SESSION, + ) + ], + contentType: ContentType::JSON + ) + ]) + ->label('abuse-limit', 10) + ->label('abuse-key', 'url:{url},challengeId:{param-challengeId}') + ->param('challengeId', '', new Text(256), 'ID of the challenge.') + ->param('otp', '', new Text(256), 'Valid verification token.') + ->inject('project') + ->inject('response') + ->inject('user') + ->inject('session') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action( + string $challengeId, + string $otp, + Document $project, + Response $response, + Document $user, + Document $session, + Database $dbForProject, + Event $queueForEvents + ): void { + $challenge = $dbForProject->getDocument('challenges', $challengeId); + + if ($challenge->isEmpty()) { + throw new Exception(Exception::USER_INVALID_TOKEN); + } + + $type = $challenge->getAttribute('type'); + + $recoveryCodeChallenge = function (Document $challenge, Document $user, string $otp) use ($dbForProject) { + if ( + $challenge->isSet('type') && + $challenge->getAttribute('type') === \strtolower(Type::RECOVERY_CODE) + ) { + $mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []); + if (\in_array($otp, $mfaRecoveryCodes)) { + $mfaRecoveryCodes = \array_diff($mfaRecoveryCodes, [$otp]); + $mfaRecoveryCodes = \array_values($mfaRecoveryCodes); + $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); + $dbForProject->updateDocument('users', $user->getId(), $user); + + return true; + } + + return false; + } + + return false; + }; + + $success = (match ($type) { + Type::TOTP => Challenge\TOTP::challenge($challenge, $user, $otp), + Type::PHONE => Challenge\Phone::challenge($challenge, $user, $otp), + Type::EMAIL => Challenge\Email::challenge($challenge, $user, $otp), + \strtolower(Type::RECOVERY_CODE) => $recoveryCodeChallenge($challenge, $user, $otp), + default => false + }); + + if (!$success) { + throw new Exception(Exception::USER_INVALID_TOKEN); + } + + $dbForProject->deleteDocument('challenges', $challengeId); + $dbForProject->purgeCachedDocument('users', $user->getId()); + + $factors = $session->getAttribute('factors', []); + $factors[] = $type; + $factors = \array_values(\array_unique($factors)); + + $session + ->setAttribute('factors', $factors) + ->setAttribute('mfaUpdatedAt', DateTime::now()); + + $dbForProject->updateDocument('sessions', $session->getId(), $session); + + $queueForEvents + ->setParam('userId', $user->getId()) + ->setParam('sessionId', $session->getId()); + + $response->dynamic($session, Response::MODEL_SESSION); + } +} diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Factors/XList.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Factors/XList.php new file mode 100644 index 0000000000..c60f599cfc --- /dev/null +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Factors/XList.php @@ -0,0 +1,89 @@ +<?php + +namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\Factors; + +use Appwrite\Auth\MFA\Type; +use Appwrite\Auth\MFA\Type\TOTP; +use Appwrite\SDK\AuthType; +use Appwrite\SDK\ContentType; +use Appwrite\SDK\Deprecated; +use Appwrite\SDK\Method; +use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Response; +use Utopia\Database\Document; +use Utopia\Platform\Action; +use Utopia\Platform\Scope\HTTP; + +class XList extends Action +{ + use HTTP; + + public static function getName(): string + { + return 'listMFAFactors'; + } + + public function __construct() + { + $this + ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/account/mfa/factors') + ->desc('List factors') + ->groups(['api', 'account', 'mfa']) + ->label('scope', 'account') + ->label('sdk', [ + new Method( + namespace: 'account', + group: 'mfa', + name: 'listMfaFactors', + description: '/docs/references/account/list-mfa-factors.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_MFA_FACTORS, + ) + ], + contentType: ContentType::JSON, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'account.listMFAFactors', + ), + ), + new Method( + namespace: 'account', + group: 'mfa', + name: 'listMFAFactors', + description: '/docs/references/account/list-mfa-factors.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_MFA_FACTORS, + ) + ], + contentType: ContentType::JSON + ) + ]) + ->inject('response') + ->inject('user') + ->callback($this->action(...)); + } + + public function action(Response $response, Document $user): void + { + $mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []); + $recoveryCodeEnabled = \is_array($mfaRecoveryCodes) && \count($mfaRecoveryCodes) > 0; + + $totp = TOTP::getAuthenticatorFromUser($user); + + $factors = new Document([ + Type::TOTP => $totp !== null && $totp->getAttribute('verified', false), + Type::EMAIL => $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false), + Type::PHONE => $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false), + Type::RECOVERY_CODE => $recoveryCodeEnabled + ]); + + $response->dynamic($factors, Response::MODEL_MFA_FACTORS); + } +} diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Create.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Create.php new file mode 100644 index 0000000000..fc26142991 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Create.php @@ -0,0 +1,105 @@ +<?php + +namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\RecoveryCodes; + +use Appwrite\Auth\MFA\Type; +use Appwrite\Event\Event; +use Appwrite\Extend\Exception; +use Appwrite\SDK\AuthType; +use Appwrite\SDK\ContentType; +use Appwrite\SDK\Deprecated; +use Appwrite\SDK\Method; +use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Response; +use Utopia\Database\Database; +use Utopia\Database\Document; +use Utopia\Platform\Action; +use Utopia\Platform\Scope\HTTP; + +class Create extends Action +{ + use HTTP; + + public static function getName(): string + { + return 'createMFARecoveryCodes'; + } + + public function __construct() + { + $this + ->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/account/mfa/recovery-codes') + ->desc('Create MFA recovery codes') + ->groups(['api', 'account']) + ->label('event', 'users.[userId].update.mfa') + ->label('scope', 'account') + ->label('audits.event', 'user.update') + ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') + ->label('sdk', [ + new Method( + namespace: 'account', + group: 'mfa', + name: 'createMfaRecoveryCodes', + description: '/docs/references/account/create-mfa-recovery-codes.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_CREATED, + model: Response::MODEL_MFA_RECOVERY_CODES, + ) + ], + contentType: ContentType::JSON, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'account.createMFARecoveryCodes', + ), + ), + new Method( + namespace: 'account', + group: 'mfa', + name: 'createMFARecoveryCodes', + description: '/docs/references/account/create-mfa-recovery-codes.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_CREATED, + model: Response::MODEL_MFA_RECOVERY_CODES, + ) + ], + contentType: ContentType::JSON + ) + ]) + ->inject('response') + ->inject('user') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action( + Response $response, + Document $user, + Database $dbForProject, + Event $queueForEvents + ): void { + $mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []); + + if (!empty($mfaRecoveryCodes)) { + throw new Exception(Exception::USER_RECOVERY_CODES_ALREADY_EXISTS); + } + + $mfaRecoveryCodes = Type::generateBackupCodes(); + $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); + $dbForProject->updateDocument('users', $user->getId(), $user); + + $queueForEvents->setParam('userId', $user->getId()); + + $document = new Document([ + 'recoveryCodes' => $mfaRecoveryCodes + ]); + + $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); + } +} diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Get.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Get.php new file mode 100644 index 0000000000..8a85c361ca --- /dev/null +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Get.php @@ -0,0 +1,86 @@ +<?php + +namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\RecoveryCodes; + +use Appwrite\Extend\Exception; +use Appwrite\SDK\AuthType; +use Appwrite\SDK\ContentType; +use Appwrite\SDK\Deprecated; +use Appwrite\SDK\Method; +use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Response; +use Utopia\Database\Document; +use Utopia\Platform\Action; +use Utopia\Platform\Scope\HTTP; + +class Get extends Action +{ + use HTTP; + + public static function getName(): string + { + return 'getMFARecoveryCodes'; + } + + public function __construct() + { + $this + ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/account/mfa/recovery-codes') + ->desc('List MFA recovery codes') + ->groups(['api', 'account', 'mfaProtected']) + ->label('scope', 'account') + ->label('sdk', [ + new Method( + namespace: 'account', + group: 'mfa', + name: 'getMfaRecoveryCodes', + description: '/docs/references/account/get-mfa-recovery-codes.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_MFA_RECOVERY_CODES, + ) + ], + contentType: ContentType::JSON, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'account.getMFARecoveryCodes', + ), + ), + new Method( + namespace: 'account', + group: 'mfa', + name: 'getMFARecoveryCodes', + description: '/docs/references/account/get-mfa-recovery-codes.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_MFA_RECOVERY_CODES, + ) + ], + contentType: ContentType::JSON + ) + ]) + ->inject('response') + ->inject('user') + ->callback($this->action(...)); + } + + public function action(Response $response, Document $user): void + { + $mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []); + + if (empty($mfaRecoveryCodes)) { + throw new Exception(Exception::USER_RECOVERY_CODES_NOT_FOUND); + } + + $document = new Document([ + 'recoveryCodes' => $mfaRecoveryCodes + ]); + + $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); + } +} diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Update.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Update.php new file mode 100644 index 0000000000..5cc2783e75 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Update.php @@ -0,0 +1,104 @@ +<?php + +namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\RecoveryCodes; + +use Appwrite\Auth\MFA\Type; +use Appwrite\Event\Event; +use Appwrite\Extend\Exception; +use Appwrite\SDK\AuthType; +use Appwrite\SDK\ContentType; +use Appwrite\SDK\Deprecated; +use Appwrite\SDK\Method; +use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Response; +use Utopia\Database\Database; +use Utopia\Database\Document; +use Utopia\Platform\Action; +use Utopia\Platform\Scope\HTTP; + +class Update extends Action +{ + use HTTP; + + public static function getName(): string + { + return 'updateMFARecoveryCodes'; + } + + public function __construct() + { + $this + ->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/account/mfa/recovery-codes') + ->desc('Update MFA recovery codes (regenerate)') + ->groups(['api', 'account', 'mfaProtected']) + ->label('event', 'users.[userId].update.mfa') + ->label('scope', 'account') + ->label('audits.event', 'user.update') + ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') + ->label('sdk', [ + new Method( + namespace: 'account', + group: 'mfa', + name: 'updateMfaRecoveryCodes', + description: '/docs/references/account/update-mfa-recovery-codes.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_MFA_RECOVERY_CODES, + ) + ], + contentType: ContentType::JSON, + deprecated: new Deprecated( + since: '1.8.0', + replaceWith: 'account.updateMFARecoveryCodes', + ), + ), + new Method( + namespace: 'account', + group: 'mfa', + name: 'updateMFARecoveryCodes', + description: '/docs/references/account/update-mfa-recovery-codes.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_MFA_RECOVERY_CODES, + ) + ], + contentType: ContentType::JSON + ) + ]) + ->inject('dbForProject') + ->inject('response') + ->inject('user') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action( + Database $dbForProject, + Response $response, + Document $user, + Event $queueForEvents + ): void { + $mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []); + if (empty($mfaRecoveryCodes)) { + throw new Exception(Exception::USER_RECOVERY_CODES_NOT_FOUND); + } + + $mfaRecoveryCodes = Type::generateBackupCodes(); + $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); + $dbForProject->updateDocument('users', $user->getId(), $user); + + $queueForEvents->setParam('userId', $user->getId()); + + $document = new Document([ + 'recoveryCodes' => $mfaRecoveryCodes + ]); + + $response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES); + } +} diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Update.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Update.php new file mode 100644 index 0000000000..00068c7441 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Update.php @@ -0,0 +1,99 @@ +<?php + +namespace Appwrite\Platform\Modules\Account\Http\Account\MFA; + +use Appwrite\Auth\MFA\Type; +use Appwrite\Auth\MFA\Type\TOTP; +use Appwrite\Event\Event; +use Appwrite\SDK\AuthType; +use Appwrite\SDK\ContentType; +use Appwrite\SDK\Method; +use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Response; +use Utopia\Database\Database; +use Utopia\Database\Document; +use Utopia\Platform\Action; +use Utopia\Platform\Scope\HTTP; +use Utopia\Validator\Boolean; + +class Update extends Action +{ + use HTTP; + + public static function getName(): string + { + return 'updateMFA'; + } + + public function __construct() + { + $this + ->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/account/mfa') + ->desc('Update MFA') + ->groups(['api', 'account']) + ->label('event', 'users.[userId].update.mfa') + ->label('scope', 'account') + ->label('audits.event', 'user.update') + ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') + ->label('sdk', new Method( + namespace: 'account', + group: 'mfa', + name: 'updateMFA', + description: '/docs/references/account/update-mfa.md', + auth: [AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_USER, + ) + ], + contentType: ContentType::JSON + )) + ->param('mfa', null, new Boolean(), 'Enable or disable MFA.') + ->inject('requestTimestamp') + ->inject('response') + ->inject('user') + ->inject('session') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action( + bool $mfa, + ?\DateTime $requestTimestamp, + Response $response, + Document $user, + Document $session, + Database $dbForProject, + Event $queueForEvents + ): void { + $user->setAttribute('mfa', $mfa); + + $user = $dbForProject->updateDocument('users', $user->getId(), $user); + + if ($mfa) { + $factors = $session->getAttribute('factors', []); + $totp = TOTP::getAuthenticatorFromUser($user); + if ($totp !== null && $totp->getAttribute('verified', false)) { + $factors[] = Type::TOTP; + } + if ($user->getAttribute('email', false) && $user->getAttribute('emailVerification', false)) { + $factors[] = Type::EMAIL; + } + if ($user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false)) { + $factors[] = Type::PHONE; + } + $factors = \array_values(\array_unique($factors)); + + $session->setAttribute('factors', $factors); + $dbForProject->updateDocument('sessions', $session->getId(), $session); + } + + $queueForEvents->setParam('userId', $user->getId()); + + $response->dynamic($user, Response::MODEL_ACCOUNT); + } +} diff --git a/src/Appwrite/Platform/Modules/Account/Module.php b/src/Appwrite/Platform/Modules/Account/Module.php new file mode 100644 index 0000000000..3ad50d388a --- /dev/null +++ b/src/Appwrite/Platform/Modules/Account/Module.php @@ -0,0 +1,14 @@ +<?php + +namespace Appwrite\Platform\Modules\Account; + +use Appwrite\Platform\Modules\Account\Services\Http; +use Utopia\Platform; + +class Module extends Platform\Module +{ + public function __construct() + { + $this->addService('http', new Http()); + } +} diff --git a/src/Appwrite/Platform/Modules/Account/Services/Http.php b/src/Appwrite/Platform/Modules/Account/Services/Http.php new file mode 100644 index 0000000000..ae2e841636 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Account/Services/Http.php @@ -0,0 +1,34 @@ +<?php + +namespace Appwrite\Platform\Modules\Account\Services; + +use Appwrite\Platform\Modules\Account\Http\Account\MFA\Authenticators\Create as CreateAuthenticator; +use Appwrite\Platform\Modules\Account\Http\Account\MFA\Authenticators\Delete as DeleteAuthenticator; +use Appwrite\Platform\Modules\Account\Http\Account\MFA\Authenticators\Update as UpdateAuthenticator; +use Appwrite\Platform\Modules\Account\Http\Account\MFA\Challenges\Create as CreateChallenge; +use Appwrite\Platform\Modules\Account\Http\Account\MFA\Challenges\Update as UpdateChallenge; +use Appwrite\Platform\Modules\Account\Http\Account\MFA\Factors\XList as ListFactors; +use Appwrite\Platform\Modules\Account\Http\Account\MFA\RecoveryCodes\Create as CreateRecoveryCodes; +use Appwrite\Platform\Modules\Account\Http\Account\MFA\RecoveryCodes\Get as GetRecoveryCodes; +use Appwrite\Platform\Modules\Account\Http\Account\MFA\RecoveryCodes\Update as UpdateRecoveryCodes; +use Appwrite\Platform\Modules\Account\Http\Account\MFA\Update as UpdateMfa; +use Utopia\Platform\Service; + +class Http extends Service +{ + public function __construct() + { + $this->type = Service::TYPE_HTTP; + $this + ->addAction(UpdateMfa::getName(), new UpdateMfa()) + ->addAction(ListFactors::getName(), new ListFactors()) + ->addAction(CreateAuthenticator::getName(), new CreateAuthenticator()) + ->addAction(UpdateAuthenticator::getName(), new UpdateAuthenticator()) + ->addAction(DeleteAuthenticator::getName(), new DeleteAuthenticator()) + ->addAction(CreateRecoveryCodes::getName(), new CreateRecoveryCodes()) + ->addAction(UpdateRecoveryCodes::getName(), new UpdateRecoveryCodes()) + ->addAction(GetRecoveryCodes::getName(), new GetRecoveryCodes()) + ->addAction(CreateChallenge::getName(), new CreateChallenge()) + ->addAction(UpdateChallenge::getName(), new UpdateChallenge()); + } +} diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php index 75e029996a..80ae11cd2f 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute; -use Appwrite\Auth\Auth; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; @@ -12,6 +11,7 @@ use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Response as UtopiaResponse; use InvalidArgumentException; use Utopia\Database\Database; @@ -91,8 +91,8 @@ class Decrement extends Action public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void { - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php index a03b30ae8e..fcf0a467fe 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute; -use Appwrite\Auth\Auth; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; @@ -12,6 +11,7 @@ use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Response as UtopiaResponse; use InvalidArgumentException; use Utopia\Database\Database; @@ -91,8 +91,8 @@ class Increment extends Action public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void { - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 6850f2b1a1..0130d8ae88 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; -use Appwrite\Auth\Auth; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; @@ -12,6 +11,7 @@ use Appwrite\SDK\Deprecated; use Appwrite\SDK\Method; use Appwrite\SDK\Parameter; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; @@ -181,8 +181,8 @@ class Create extends Action $documents = [$data]; } - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($isBulk && !$isAPIKey && !$isPrivilegedUser) { throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index 15fa5719dd..85b02297c3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; -use Appwrite\Auth\Auth; use Appwrite\Databases\TransactionState; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; @@ -12,6 +11,7 @@ use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; @@ -105,8 +105,8 @@ class Delete extends Action ): void { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php index a8f89289db..3f4a2371d0 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; -use Appwrite\Auth\Auth; use Appwrite\Databases\TransactionState; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; @@ -11,6 +10,7 @@ use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Exception\Query as QueryException; @@ -77,8 +77,8 @@ class Get extends Action public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, StatsUsage $queueForStatsUsage, TransactionState $transactionState): void { - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php index 9ef7ed4b35..63c4a27c7e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; -use Appwrite\Auth\Auth; use Appwrite\Databases\TransactionState; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; @@ -12,6 +11,7 @@ use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; @@ -103,8 +103,8 @@ class Update extends Action $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php index d0f1ca4499..9cfb3d337d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; -use Appwrite\Auth\Auth; use Appwrite\Databases\TransactionState; use Appwrite\Event\Event; use Appwrite\Event\StatsUsage; @@ -12,6 +11,7 @@ use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; @@ -109,8 +109,8 @@ class Upsert extends Action throw new Exception($this->getMissingPayloadException()); } - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index f5c94ba443..f9bc24c41e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents; -use Appwrite\Auth\Auth; use Appwrite\Databases\TransactionState; use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; @@ -11,6 +10,7 @@ use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; @@ -81,8 +81,8 @@ class XList extends Action public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, StatsUsage $queueForStatsUsage, TransactionState $transactionState): void { - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index 58db4b4229..029d55810c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Operations; -use Appwrite\Auth\Auth; use Appwrite\Databases\TransactionState; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Action; @@ -10,6 +9,7 @@ use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Database\Validator\Operation; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; @@ -72,8 +72,8 @@ class Create extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Operations array cannot be empty'); } - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); // API keys and admins can read any transaction, regular users need permissions $transaction = ($isAPIKey || $isPrivilegedUser) diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index d305c35bba..d4f62a3d7b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions; -use Appwrite\Auth\Auth; use Appwrite\Databases\TransactionState; use Appwrite\Event\Delete; use Appwrite\Event\Event; @@ -12,6 +11,7 @@ use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; use Utopia\Database\Document; @@ -112,8 +112,8 @@ class Update extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Cannot commit and rollback at the same time'); } - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $transaction = ($isAPIKey || $isPrivilegedUser) ? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId)) @@ -242,13 +242,12 @@ class Update extends Action ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($transaction); }); - } catch (NotFoundException $e) { Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); throw new Exception(Exception::DOCUMENT_NOT_FOUND, previous: $e); - } catch (DuplicateException|ConflictException $e) { + } catch (DuplicateException | ConflictException $e) { Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([ 'status' => 'failed', ]))); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index bd964083f2..62f826576a 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -3,7 +3,6 @@ namespace Appwrite\Platform\Modules\Functions\Http\Executions; use Ahc\Jwt\JWT; -use Appwrite\Auth\Auth; use Appwrite\Event\Event; use Appwrite\Event\Func; use Appwrite\Event\StatsUsage; @@ -15,9 +14,12 @@ use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Response; use Executor\Executor; use MaxMind\Db\Reader; +use Utopia\Auth\Proofs\Token; +use Utopia\Auth\Store; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Database; @@ -93,6 +95,8 @@ class Create extends Base ->inject('queueForStatsUsage') ->inject('queueForFunctions') ->inject('geodb') + ->inject('store') + ->inject('proofForToken') ->inject('executor') ->callback($this->action(...)); } @@ -115,6 +119,8 @@ class Create extends Base StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, + Store $store, + Token $proofForToken, Executor $executor ) { $async = \strval($async) === 'true' || \strval($async) === '1'; @@ -155,8 +161,8 @@ class Create extends Base $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); @@ -199,7 +205,7 @@ class Create extends Base foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */ - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + if ($proofForToken->verify($store->getProperty('secret', ''), $session->getAttribute('secret'))) { // Find most recent active session for user ID and JWT headers $current = $session; } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php index d85e4a4f70..5f06305d3f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php @@ -2,12 +2,12 @@ namespace Appwrite\Platform\Modules\Functions\Http\Executions; -use Appwrite\Auth\Auth; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Validator\Authorization; @@ -63,8 +63,8 @@ class Get extends Base ) { $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php index 4ac766f79f..f27e258543 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php @@ -2,12 +2,12 @@ namespace Appwrite\Platform\Modules\Functions\Http\Executions; -use Appwrite\Auth\Auth; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Database\Validator\Queries\Executions; use Appwrite\Utopia\Response; use Utopia\Database\Database; @@ -72,8 +72,8 @@ class XList extends Base ) { $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php index 5708f1b83b..f79dece530 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Modules\Tokens\Http\Tokens\Buckets\Files; -use Appwrite\Auth\Auth; use Appwrite\Extend\Exception; +use Appwrite\Utopia\Database\Documents\User; use Utopia\Database\Database; use Utopia\Database\Validator\Authorization; use Utopia\Platform\Action as UtopiaAction; @@ -14,8 +14,8 @@ class Action extends UtopiaAction { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $isAPIKey = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php index 0b1a2c63ec..66af705f2b 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Modules\Tokens\Http\Tokens\Buckets\Files; -use Appwrite\Auth\Auth; use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; @@ -10,6 +9,7 @@ use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; +use Utopia\Auth\Proofs\Token; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; @@ -91,7 +91,7 @@ class Create extends Action $token = $dbForProject->createDocument('resourceTokens', new Document([ '$id' => ID::unique(), - 'secret' => Auth::tokenGenerator(128), + 'secret' => (new Token(128))->generate(), 'resourceId' => $bucketId . ':' . $fileId, 'resourceInternalId' => $bucket->getSequence() . ':' . $file->getSequence(), 'resourceType' => TOKENS_RESOURCE_TYPE_FILES, diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index 1b1af97eca..1a269fecf7 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -2,10 +2,11 @@ namespace Appwrite\Platform\Tasks; -use Appwrite\Auth\Auth; use Appwrite\Docker\Compose; use Appwrite\Docker\Env; use Appwrite\Utopia\View; +use Utopia\Auth\Proofs\Password; +use Utopia\Auth\Proofs\Token; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Platform\Action; @@ -150,6 +151,8 @@ class Install extends Action $input = []; + $password = new Password(); + $token = new Token(); foreach ($vars as $var) { if (!empty($var['filter']) && ($interactive !== 'Y' || !Console::isInteractive())) { if ($data && $var['default'] !== null) { @@ -158,12 +161,12 @@ class Install extends Action } if ($var['filter'] === 'token') { - $input[$var['name']] = Auth::tokenGenerator(); + $input[$var['name']] = $token->generate(); continue; } if ($var['filter'] === 'password') { - $input[$var['name']] = Auth::passwordGenerator(); + $input[$var['name']] = $password->generate(); continue; } } diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php index a88e2e641f..be542e7811 100644 --- a/src/Appwrite/Platform/Workers/Audits.php +++ b/src/Appwrite/Platform/Workers/Audits.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Workers; -use Appwrite\Auth\Auth; use Exception; use Throwable; use Utopia\Audit\Audit; @@ -85,7 +84,7 @@ class Audits extends Action $userName = $user->getAttribute('name', ''); $userEmail = $user->getAttribute('email', ''); - $userType = $user->getAttribute('type', Auth::ACTIVITY_TYPE_USER); + $userType = $user->getAttribute('type', ACTIVITY_TYPE_USER); // Create event data $eventData = [ diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 7df2770ac6..38624367c9 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -2,7 +2,6 @@ namespace Appwrite\Platform\Workers; -use Appwrite\Auth\Auth; use Appwrite\Certificates\Adapter as CertificatesAdapter; use Appwrite\Deletes\Identities; use Appwrite\Deletes\Targets; @@ -711,7 +710,7 @@ class Deletes extends Action private function deleteExpiredSessions(Document $project, callable $getProjectDB): void { $dbForProject = $getProjectDB($project); - $duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; $expired = DateTime::addSeconds(new \DateTime(), -1 * $duration); // Delete Sessions diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 05bad48927..43f901158e 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -566,6 +566,7 @@ class Migrations extends Action 'fileId' => $fileId, 'projectId' => $project->getId(), 'internal' => true, + 'disposition' => 'attachment', ]); // Generate download URL with JWT diff --git a/src/Appwrite/Utopia/Database/Documents/User.php b/src/Appwrite/Utopia/Database/Documents/User.php new file mode 100644 index 0000000000..a85b0a897c --- /dev/null +++ b/src/Appwrite/Utopia/Database/Documents/User.php @@ -0,0 +1,182 @@ +<?php + +namespace Appwrite\Utopia\Database\Documents; + +use Utopia\Auth\Proof; +use Utopia\Auth\Proofs\Token; +use Utopia\Database\DateTime; +use Utopia\Database\Document; +use Utopia\Database\Helpers\Role; +use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Roles; + +class User extends Document +{ + public const ROLE_ANY = 'any'; + public const ROLE_GUESTS = 'guests'; + public const ROLE_USERS = 'users'; + public const ROLE_ADMIN = 'admin'; + public const ROLE_DEVELOPER = 'developer'; + public const ROLE_OWNER = 'owner'; + public const ROLE_APPS = 'apps'; + public const ROLE_SYSTEM = 'system'; + + public function getEmail(): ?string + { + return $this->getAttribute('email'); + } + + public function getPhone(): ?string + { + return $this->getAttribute('phone'); + } + + /** + * Returns all roles for a user. + * + * @return array<string> + */ + public function getRoles(): array + { + $roles = []; + + if (!$this->isPrivileged(Authorization::getRoles()) && !$this->isApp(Authorization::getRoles())) { + if ($this->getId()) { + $roles[] = Role::user($this->getId())->toString(); + $roles[] = Role::users()->toString(); + + $emailVerified = $this->getAttribute('emailVerification', false); + $phoneVerified = $this->getAttribute('phoneVerification', false); + + if ($emailVerified || $phoneVerified) { + $roles[] = Role::user($this->getId(), Roles::DIMENSION_VERIFIED)->toString(); + $roles[] = Role::users(Roles::DIMENSION_VERIFIED)->toString(); + } else { + $roles[] = Role::user($this->getId(), Roles::DIMENSION_UNVERIFIED)->toString(); + $roles[] = Role::users(Roles::DIMENSION_UNVERIFIED)->toString(); + } + } else { + return [Role::guests()->toString()]; + } + } + + foreach ($this->getAttribute('memberships', []) as $node) { + if (!isset($node['confirm']) || !$node['confirm']) { + continue; + } + + if (isset($node['$id']) && isset($node['teamId'])) { + $roles[] = Role::team($node['teamId'])->toString(); + $roles[] = Role::member($node['$id'])->toString(); + + if (isset($node['roles'])) { + foreach ($node['roles'] as $nodeRole) { // Set all team roles + $roles[] = Role::team($node['teamId'], $nodeRole)->toString(); + } + } + } + } + + foreach ($this->getAttribute('labels', []) as $label) { + $roles[] = 'label:' . $label; + } + + return $roles; + } + + /** + * Check if user is anonymous. + * + * @param Document $this + * @return bool + */ + public function isAnonymous(): bool + { + return is_null($this->getEmail()) + && is_null($this->getPhone()); + } + + /** + * Is Privileged User? + * + * @param array<string> $roles + * + * @return bool + */ + public static function isPrivileged(array $roles): bool + { + if ( + in_array(self::ROLE_OWNER, $roles) || + in_array(self::ROLE_DEVELOPER, $roles) || + in_array(self::ROLE_ADMIN, $roles) + ) { + return true; + } + + return false; + } + + /** + * Is App User? + * + * @param array<string> $roles + * + * @return bool + */ + public static function isApp(array $roles): bool + { + if (in_array(self::ROLE_APPS, $roles)) { + return true; + } + + return false; + } + + public function tokenVerify(int $type = null, string $secret, Proof $proofForToken): false|Document + { + $tokens = $this->getAttribute('tokens', []); + foreach ($tokens as $token) { + if ( + $token->isSet('secret') && + $token->isSet('expire') && + $token->isSet('type') && + ($type === null || $token->getAttribute('type') === $type) && + $proofForToken->verify($secret, $token->getAttribute('secret')) && + DateTime::formatTz($token->getAttribute('expire')) >= DateTime::formatTz(DateTime::now()) + ) { + return $token; + } + } + + return false; + } + + /** + * Verify session and check that its not expired. + * + * @param array<Document> $sessions + * @param string $secret + * + * @return bool|string + */ + public function sessionVerify(string $secret, Token $proofForToken) + { + $sessions = $this->getAttribute('sessions', []); + + foreach ($sessions as $session) { + if ( + $session->isSet('secret') && + $session->isSet('provider') && + $session->isSet('expire') && + $proofForToken->verify($secret, $session->getAttribute('secret')) && + DateTime::formatTz(DateTime::format(new \DateTime($session->getAttribute('expire')))) >= DateTime::formatTz(DateTime::now()) + ) { + return $session->getId(); + } + } + + return false; + + return false; + } +} diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index 558f0cdf09..ce570d2af9 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -2,8 +2,8 @@ namespace Appwrite\Utopia; -use Appwrite\Auth\Auth; use Appwrite\SDK\Method; +use Appwrite\Utopia\Database\Documents\User; use Appwrite\Utopia\Request\Filter; use Swoole\Http\Request as SwooleRequest; use Utopia\Database\Validator\Authorization; @@ -199,19 +199,19 @@ class Request extends UtopiaRequest } /** - * Get User Agent - * - * Method for getting User Agent. Preferring forwarded agent for privileged users; otherwise returns default. - * - * @param string $default - * @return string - */ + * Get User Agent + * + * Method for getting User Agent. Preferring forwarded agent for privileged users; otherwise returns default. + * + * @param string $default + * @return string + */ public function getUserAgent(string $default = ''): string { $forwardedUserAgent = $this->getHeader('x-forwarded-user-agent'); if (!empty($forwardedUserAgent)) { $roles = Authorization::getRoles(); - $isAppUser = Auth::isAppUser($roles); + $isAppUser = User::isApp($roles); if ($isAppUser) { return $forwardedUserAgent; diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 94b2a799a6..2dc5f727f7 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -2,7 +2,7 @@ namespace Appwrite\Utopia; -use Appwrite\Auth\Auth; +use Appwrite\Utopia\Database\Documents\User as DBUser; use Appwrite\Utopia\Fetch\BodyMultipart; use Appwrite\Utopia\Response\Filter; use Appwrite\Utopia\Response\Model; @@ -150,8 +150,8 @@ use Appwrite\Utopia\Response\Model\VectorDBCollection; use Appwrite\Utopia\Response\Model\Webhook; use Exception; use JsonException; -use Swoole\Http\Response as SwooleHTTPResponse; // Keep last +use Swoole\Http\Response as SwooleHTTPResponse; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Swoole\Response as SwooleResponse; @@ -829,8 +829,8 @@ class Response extends SwooleResponse if ($rule['sensitive']) { $roles = Authorization::getRoles(); - $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); + $isPrivilegedUser = DBUser::isPrivileged($roles); + $isAppUser = DBUser::isApp($roles); if ((!$isPrivilegedUser && !$isAppUser) && !self::$showSensitive) { $data->setAttribute($key, ''); diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index abe67e7e86..65f9f7685b 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -2,7 +2,6 @@ namespace Appwrite\Utopia\Response\Model; -use Appwrite\Auth\Auth; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model; use Utopia\Config\Config; @@ -105,7 +104,7 @@ class Project extends Model ->addRule('authDuration', [ 'type' => self::TYPE_INTEGER, 'description' => 'Session duration in seconds.', - 'default' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, + 'default' => TOKEN_EXPIRATION_LOGIN_LONG, 'example' => 60, ]) ->addRule('authLimit', [ @@ -372,7 +371,7 @@ class Project extends Model $auth = Config::getParam('auth', []); $document->setAttribute('authLimit', $authValues['limit'] ?? 0); - $document->setAttribute('authDuration', $authValues['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG); + $document->setAttribute('authDuration', $authValues['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG); $document->setAttribute('authSessionsLimit', $authValues['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT); $document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0); $document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false); diff --git a/src/Appwrite/Utopia/Response/Model/ResourceToken.php b/src/Appwrite/Utopia/Response/Model/ResourceToken.php index 87ab66ab5d..c2b3deb56b 100644 --- a/src/Appwrite/Utopia/Response/Model/ResourceToken.php +++ b/src/Appwrite/Utopia/Response/Model/ResourceToken.php @@ -64,7 +64,7 @@ class ResourceToken extends Model $expire = $document->getAttribute('expire'); // Use a large but reasonable maxAge to avoid auto-exp when we set explicit exp - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400 * 365 * 10, 10); // 10 years + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), RESOURCE_TOKEN_ALGORITHM, RESOURCE_TOKEN_MAX_AGE, RESOURCE_TOKEN_LEEWAY); // 10 years $payload = [ 'tokenId' => $document->getId(), @@ -73,13 +73,13 @@ class ResourceToken extends Model 'resourceInternalId' => $document->getAttribute('resourceInternalId'), ]; + $createdDate = new \DateTime($document->getCreatedAt()); + $payload['iat'] = $createdDate->getTimestamp(); + // Set explicit expiration in JWT payload if we have an expiry date if ($expire !== null) { $expiryDate = new \DateTime($expire); $payload['exp'] = $expiryDate->getTimestamp(); - } else { - // For infinite expiry, set 'iat' to prevent JWT library from auto-adding 'exp' - $payload['iat'] = time(); } $secret = $jwt->encode($payload); diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index 9f35932700..b217608395 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -55,7 +55,7 @@ trait AccountBase 'origin' => 'http://localhost', 'content-type' => 'application/json', 'x-appwrite-project' => 'console', - 'x-forwarded-for' => '103.152.127.250' // Test IP for denied access region + 'x-forwarded-for' => '31.6.14.220' // Test IP for denied access region ]), [ 'userId' => ID::unique(), 'email' => $email, diff --git a/tests/e2e/Services/Avatars/AvatarsBase.php b/tests/e2e/Services/Avatars/AvatarsBase.php index cc626be99a..aca3af6dfa 100644 --- a/tests/e2e/Services/Avatars/AvatarsBase.php +++ b/tests/e2e/Services/Avatars/AvatarsBase.php @@ -1293,4 +1293,32 @@ trait AvatarsBase return []; } + + public function testGetScreenshotComparison(): array + { + /** + * Test screenshot comparison with stable domain (example.com) + * This test captures a screenshot of example.com and compares it + * against a reference image to ensure consistent rendering. + */ + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://example.com', + 'width' => 800, + 'height' => 600, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + // Compare with reference screenshot + $referencePath = \realpath(__DIR__ . '/../../../resources/avatars'); + $referenceScreenshot = $referencePath . '/screenshot-example-com.png'; + $this->assertFileExists($referenceScreenshot, 'Reference example.com screenshot not found'); + $this->assertSamePixels($referenceScreenshot, $response['body']); + + return []; + } } diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index b50ee5e14c..8fd53ba422 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -1336,7 +1336,7 @@ trait MigrationsBase $this->assertEquals('CSV', $response['body']['destination']); return true; - }, 30000, 500); + }, 30_000, 500); // Check that email was sent with download link $lastEmail = $this->getLastEmail(); diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 8aa4f19e70..8eefcfe5ad 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -2,7 +2,6 @@ namespace Tests\E2E\Services\Projects; -use Appwrite\Auth\Auth; use Appwrite\Extend\Exception; use Appwrite\Tests\Async; use Tests\E2E\Client; @@ -869,7 +868,7 @@ class ProjectsConsoleClientTest extends Scope ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(Auth::TOKEN_EXPIRATION_LOGIN_LONG, $response['body']['authDuration']); // 1 Year + $this->assertEquals(TOKEN_EXPIRATION_LOGIN_LONG, $response['body']['authDuration']); // 1 Year /** * Test for SUCCESS @@ -1012,7 +1011,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, + 'duration' => TOKEN_EXPIRATION_LOGIN_LONG, ]); $this->assertEquals(200, $response['headers']['status-code']); @@ -1025,7 +1024,7 @@ class ProjectsConsoleClientTest extends Scope ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(Auth::TOKEN_EXPIRATION_LOGIN_LONG, $response['body']['authDuration']); // 1 Year + $this->assertEquals(TOKEN_EXPIRATION_LOGIN_LONG, $response['body']['authDuration']); // 1 Year return ['projectId' => $projectId]; } diff --git a/tests/e2e/Services/Sites/SitesConsoleClientTest.php b/tests/e2e/Services/Sites/SitesConsoleClientTest.php index 227e36a50e..2b75402b25 100644 --- a/tests/e2e/Services/Sites/SitesConsoleClientTest.php +++ b/tests/e2e/Services/Sites/SitesConsoleClientTest.php @@ -74,8 +74,11 @@ class SitesConsoleClientTest extends Scope $this->assertGreaterThan(1, $file['headers']['content-length']); $this->assertEquals('image/png', $file['headers']['content-type']); - $screenshotHash = \md5($file['body']); - $this->assertNotEmpty($screenshotHash); + // Compare with reference screenshots + $referencePath = \realpath(__DIR__ . '/../../../resources/sites/static-themed'); + $referenceScreenshotLight = $referencePath . '/screenshot-light.png'; + $this->assertFileExists($referenceScreenshotLight, 'Reference light screenshot not found'); + $this->assertSamePixels($referenceScreenshotLight, $file['body']); $screenshotId = $deployment['body']['screenshotDark']; $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console", array_merge($this->getHeaders(), [ @@ -87,10 +90,9 @@ class SitesConsoleClientTest extends Scope $this->assertGreaterThan(1, $file['headers']['content-length']); $this->assertEquals('image/png', $file['headers']['content-type']); - $screenshotDarkHash = \md5($file['body']); - $this->assertNotEmpty($screenshotDarkHash); - - $this->assertNotEquals($screenshotDarkHash, $screenshotHash); + $referenceScreenshotDark = $referencePath . '/screenshot-dark.png'; + $this->assertFileExists($referenceScreenshotDark, 'Reference dark screenshot not found'); + $this->assertSamePixels($referenceScreenshotDark, $file['body']); $screenshotId = $deployment['body']['screenshotLight']; $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console"); diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php index f1480faba0..7a9181b1dc 100644 --- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php +++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php @@ -72,37 +72,45 @@ class TokensConsoleClientTest extends Scope $this->assertEquals(400, $token['headers']['status-code']); $this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']); - // Success case: No expire date - $token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'] - ], $this->getHeaders()), [ - 'expire' => null, - ]); + // Success cases: With & without expiry + $expireList = [null, date('Y-m-d', strtotime("tomorrow"))]; + foreach ($expireList as $expire) { + $token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), [ + 'expire' => $expire, + ]); - $this->assertEquals(201, $token['headers']['status-code']); - $this->assertEquals('files', $token['body']['resourceType']); - $this->assertNotEmpty($token['body']['$id']); - $this->assertNotEmpty($token['body']['secret']); + $this->assertEquals(201, $token['headers']['status-code']); + $this->assertEquals('files', $token['body']['resourceType']); + $this->assertNotEmpty($token['body']['$id']); + $this->assertNotEmpty($token['body']['secret']); - // Verify the generated token JWT contains correct resource information - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400 * 365 * 10, 10); // 10 years maxAge - try { - $payload = $jwt->decode($token['body']['secret']); - $this->assertIsArray($payload, 'JWT payload should decode to an array'); - $this->assertArrayHasKey('tokenId', $payload, 'JWT payload should contain tokenId'); - $this->assertArrayHasKey('resourceId', $payload, 'JWT payload should contain resourceId'); - $this->assertArrayHasKey('resourceType', $payload, 'JWT payload should contain resourceType'); - $this->assertArrayHasKey('resourceInternalId', $payload, 'JWT payload should contain resourceInternalId'); + // Verify the generated token JWT contains correct resource information + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400 * 365 * 10, 10); // 10 years maxAge + try { + $payload = $jwt->decode($token['body']['secret']); + $this->assertIsArray($payload, 'JWT payload should decode to an array'); + $this->assertArrayHasKey('tokenId', $payload, 'JWT payload should contain tokenId'); + $this->assertArrayHasKey('resourceId', $payload, 'JWT payload should contain resourceId'); + $this->assertArrayHasKey('resourceType', $payload, 'JWT payload should contain resourceType'); + $this->assertArrayHasKey('resourceInternalId', $payload, 'JWT payload should contain resourceInternalId'); + $this->assertArrayHasKey('iat', $payload, 'JWT payload should contain iat'); - $this->assertEquals($token['body']['$id'], $payload['tokenId'], 'JWT tokenId should match token ID'); - $this->assertEquals($bucketId . ':' . $fileId, $payload['resourceId'], 'JWT resourceId should match bucketId:fileId format'); - $this->assertEquals('files', $payload['resourceType'], 'JWT resourceType should be files'); + if (!empty($expire)) { + $this->assertArrayHasKey('exp', $payload, 'JWT payload should contain exp'); + } else { + $this->assertArrayNotHasKey('exp', $payload, 'JWT payload should not contain exp field for tokens without expiry'); + } - // For newly created tokens without expiry, should not have exp field - $this->assertArrayNotHasKey('exp', $payload, 'JWT payload should not contain exp field for tokens without expiry'); - } catch (JWTException $e) { - $this->fail('Failed to decode JWT: ' . $e->getMessage()); + $this->assertEquals($token['body']['$id'], $payload['tokenId'], 'JWT tokenId should match token ID'); + $this->assertEquals($bucketId . ':' . $fileId, $payload['resourceId'], 'JWT resourceId should match bucketId:fileId format'); + $this->assertEquals('files', $payload['resourceType'], 'JWT resourceType should be files'); + + } catch (JWTException $e) { + $this->fail('Failed to decode JWT: ' . $e->getMessage()); + } } return [ @@ -218,6 +226,11 @@ class TokensConsoleClientTest extends Scope $this->assertArrayHasKey('resourceId', $payload, 'JWT payload should contain resourceId'); $this->assertArrayHasKey('resourceType', $payload, 'JWT payload should contain resourceType'); $this->assertArrayHasKey('resourceInternalId', $payload, 'JWT payload should contain resourceInternalId'); + $this->assertArrayHasKey('iat', $payload, 'JWT payload should contain iat'); + + if (!empty($token['expire'])) { + $this->assertArrayHasKey('exp', $payload, 'JWT payload should contain exp'); + } $this->assertEquals($token['$id'], $payload['tokenId'], 'JWT tokenId should match token ID'); $this->assertEquals($data['bucketId'] . ':' . $data['fileId'], $payload['resourceId'], 'JWT resourceId should match bucketId:fileId format'); diff --git a/tests/e2e/Services/Users/UsersConsoleClientTest.php b/tests/e2e/Services/Users/UsersConsoleClientTest.php index 967104f5db..24e0f6868b 100644 --- a/tests/e2e/Services/Users/UsersConsoleClientTest.php +++ b/tests/e2e/Services/Users/UsersConsoleClientTest.php @@ -6,6 +6,7 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideConsole; +use Utopia\Database\Helpers\ID; class UsersConsoleClientTest extends Scope { @@ -45,4 +46,39 @@ class UsersConsoleClientTest extends Scope $this->assertIsArray($response['body']['users']); $this->assertIsArray($response['body']['sessions']); } + + public function testCreateUserWithoutPasswordThenSetPassword() + { + // Create a user with email but without password + $userId = ID::unique(); + $email = $userId . '@example.com'; + + $response = $this->client->call(Client::METHOD_POST, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), [ + 'userId' => $userId, + 'email' => $email, + // no password provided + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals($userId, $response['body']['$id']); + $this->assertEquals($email, $response['body']['email']); + $this->assertEmpty($response['body']['password']); + + // Now set the password for that user (console-side) + $newPassword = 'NewPass123!'; + + $set = $this->client->call(Client::METHOD_PATCH, '/users/' . $userId . '/password', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), [ + 'password' => $newPassword, + ]); + + $this->assertEquals(200, $set['headers']['status-code']); + $this->assertEquals($userId, $set['body']['$id']); + $this->assertNotEmpty($set['body']['password']); + } } diff --git a/tests/e2e/Services/VCS/VCSConsoleClientTest.php b/tests/e2e/Services/VCS/VCSConsoleClientTest.php index 13c3ddb251..963aa5a84b 100644 --- a/tests/e2e/Services/VCS/VCSConsoleClientTest.php +++ b/tests/e2e/Services/VCS/VCSConsoleClientTest.php @@ -10,6 +10,7 @@ use Utopia\Cache\Adapter\None; use Utopia\Cache\Cache; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; +use Utopia\Database\Query; use Utopia\System\System; use Utopia\VCS\Adapter\Git\GitHub; @@ -316,6 +317,43 @@ class VCSConsoleClientTest extends Scope $this->assertEquals($searchedRepositories['body']['runtimeProviderRepositories'][0]['name'], 'appwrite'); $this->assertEquals($searchedRepositories['body']['runtimeProviderRepositories'][0]['runtime'], 'other'); + // with limit and offset + $repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'type' => 'runtime', + 'limit' => Query::limit(1)->toString(), + 'offset' => Query::offset(0)->toString() + ]); + $this->assertSame(200, $repositories['headers']['status-code']); + $this->assertSame(4, $repositories['body']['total']); + $this->assertCount(1, $repositories['body']['runtimeProviderRepositories']); + $this->assertSame('starter-for-svelte', $repositories['body']['runtimeProviderRepositories'][0]['name']); + + $repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'type' => 'runtime', + 'limit' => Query::limit(2)->toString(), + 'offset' => Query::offset(2)->toString() + ]); + $this->assertSame(200, $repositories['headers']['status-code']); + $this->assertSame(4, $repositories['body']['total']); + $this->assertCount(2, $repositories['body']['runtimeProviderRepositories']); + $this->assertSame('appwrite', $repositories['body']['runtimeProviderRepositories'][0]['name']); + $this->assertSame('ruby-starter', $repositories['body']['runtimeProviderRepositories'][1]['name']); + + $repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'type' => 'runtime', + 'limit' => Query::limit(2)->toString(), + 'offset' => Query::offset(100)->toString() + ]); + $this->assertSame(200, $repositories['headers']['status-code']); + $this->assertSame(4, $repositories['body']['total']); + $this->assertCount(0, $repositories['body']['runtimeProviderRepositories']); + // TODO: If you are about to add another check, rewrite this to @provideScenarios /** @@ -338,6 +376,17 @@ class VCSConsoleClientTest extends Scope $this->assertEquals(400, $repositories['headers']['status-code']); + // invalid offset + $repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'type' => 'runtime', + 'limit' => Query::limit(2)->toString(), + 'offset' => Query::offset(1)->toString() + ]); + $this->assertEquals(400, $repositories['headers']['status-code']); + $this->assertEquals('offset must be a multiple of the limit', $repositories['body']['message']); + $repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([ 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ diff --git a/tests/resources/avatars/screenshot-example-com.png b/tests/resources/avatars/screenshot-example-com.png new file mode 100644 index 0000000000..2f8af920e0 Binary files /dev/null and b/tests/resources/avatars/screenshot-example-com.png differ diff --git a/tests/resources/sites/static-themed/index.html b/tests/resources/sites/static-themed/index.html index 955696b473..f1a446612f 100644 --- a/tests/resources/sites/static-themed/index.html +++ b/tests/resources/sites/static-themed/index.html @@ -5,19 +5,70 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Themed website - + +
+

Themed website

+

Adaptive light and dark mode showcase

+
Appwrite Sites
+
+ diff --git a/tests/resources/sites/static-themed/screenshot-dark.png b/tests/resources/sites/static-themed/screenshot-dark.png new file mode 100644 index 0000000000..199a09b902 Binary files /dev/null and b/tests/resources/sites/static-themed/screenshot-dark.png differ diff --git a/tests/resources/sites/static-themed/screenshot-light.png b/tests/resources/sites/static-themed/screenshot-light.png new file mode 100644 index 0000000000..4eae73c5b7 Binary files /dev/null and b/tests/resources/sites/static-themed/screenshot-light.png differ diff --git a/tests/unit/Auth/AuthTest.php b/tests/unit/Auth/AuthTest.php deleted file mode 100644 index 705da42879..0000000000 --- a/tests/unit/Auth/AuthTest.php +++ /dev/null @@ -1,502 +0,0 @@ -toString()); - } - - public function testCookieName(): void - { - $name = 'cookie-name'; - - $this->assertEquals(Auth::setCookieName($name), $name); - $this->assertEquals(Auth::$cookieName, $name); - } - - public function testEncodeDecodeSession(): void - { - $id = 'id'; - $secret = 'secret'; - $session = 'eyJpZCI6ImlkIiwic2VjcmV0Ijoic2VjcmV0In0='; - - $this->assertEquals(Auth::encodeSession($id, $secret), $session); - $this->assertEquals(Auth::decodeSession($session), ['id' => $id, 'secret' => $secret]); - } - - public function testHash(): void - { - $secret = 'secret'; - $this->assertEquals(Auth::hash($secret), '2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b'); - } - - public function testPassword(): void - { - /* - General tests, using pre-defined hashes generated by online tools - */ - - // Bcrypt - Version Y - $plain = 'secret'; - $hash = '$2y$08$PDbMtV18J1KOBI9tIYabBuyUwBrtXPGhLxCy9pWP6xkldVOKLrLKy'; - $generatedHash = Auth::passwordHash($plain, 'bcrypt'); - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt')); - $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt')); - $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt')); - - // Bcrypt - Version A - $plain = 'test123'; - $hash = '$2a$12$3f2ZaARQ1AmhtQWx2nmQpuXcWfTj1YV2/Hl54e8uKxIzJe3IfwLiu'; - $generatedHash = Auth::passwordHash($plain, 'bcrypt'); - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt')); - $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt')); - $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt')); - - // Bcrypt - Cost 5 - $plain = 'hello-world'; - $hash = '$2a$05$IjrtSz6SN7UJ6Sh3l.b5jODEvEG2LMJTPAHIaLWRvlWx7if3VMkFO'; - $generatedHash = Auth::passwordHash($plain, 'bcrypt'); - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt')); - $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt')); - $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt')); - - // Bcrypt - Cost 15 - $plain = 'super-secret-password'; - $hash = '$2a$15$DS0ZzbsFZYumH/E4Qj5oeOHnBcM3nCCsCA2m4Goigat/0iMVQC4Na'; - $generatedHash = Auth::passwordHash($plain, 'bcrypt'); - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt')); - $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt')); - $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt')); - - // MD5 - Short - $plain = 'appwrite'; - $hash = '144fa7eaa4904e8ee120651997f70dcc'; - $generatedHash = Auth::passwordHash($plain, 'md5'); - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md5')); - $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'md5')); - $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'md5')); - - // MD5 - Long - $plain = 'AppwriteIsAwesomeBackendAsAServiceThatIsAlsoOpenSourced'; - $hash = '8410e96cf7ac64e0b84c3f8517a82616'; - $generatedHash = Auth::passwordHash($plain, 'md5'); - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md5')); - $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'md5')); - $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'md5')); - - // PHPass - $plain = 'pass123'; - $hash = '$P$BVKPmJBZuLch27D4oiMRTEykGLQ9tX0'; - $generatedHash = Auth::passwordHash($plain, 'phpass'); - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'phpass')); - $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'phpass')); - $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'phpass')); - - // SHA - $plain = 'developersAreAwesome!'; - $hash = '2455118438cb125354b89bb5888346e9bd23355462c40df393fab514bf2220b5a08e4e2d7b85d7327595a450d0ac965cc6661152a46a157c66d681bed20a4735'; - $generatedHash = Auth::passwordHash($plain, 'sha'); - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'sha')); - $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'sha')); - $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'sha')); - - // Argon2 - $plain = 'safe-argon-password'; - $hash = '$argon2id$v=19$m=2048,t=3,p=4$MWc5NWRmc2QxZzU2$41mp7rSgBZ49YxLbbxIac7aRaxfp5/e1G45ckwnK0g8'; - $generatedHash = Auth::passwordHash($plain, 'argon2'); - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'argon2')); - $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'argon2')); - $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'argon2')); - - // Scrypt - $plain = 'some-scrypt-password'; - $hash = 'b448ad7ba88b653b5b56b8053a06806724932d0751988bc9cd0ef7ff059e8ba8a020e1913b7069a650d3f99a1559aba0221f2c277826919513a054e76e339028'; - $generatedHash = Auth::passwordHash($plain, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]); - - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2])); - $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2])); - $this->assertEquals(false, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-wrong-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2])); - $this->assertEquals(false, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 10, 'costParallel' => 2])); - $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2])); - - // ScryptModified tested are in provider-specific tests below - - /* - Provider-specific tests, ensuring functionality of specific use-cases - */ - - // Provider #1 (Database) - $plain = 'example-password'; - $hash = '$2a$10$3bIGRWUes86CICsuchGLj.e.BqdCdg2/1Ud9LvBhJr0j7Dze8PBdS'; - $generatedHash = Auth::passwordHash($plain, 'bcrypt'); - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt')); - $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt')); - $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt')); - - // Provider #2 (Blog) - $plain = 'your-password'; - $hash = '$P$BkiNDJTpAWXtpaMhEUhUdrv7M0I1g6.'; - $generatedHash = Auth::passwordHash($plain, 'phpass'); - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'phpass')); - $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'phpass')); - $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'phpass')); - - // Provider #2 (Google) - $plain = 'users-password'; - $hash = 'EPKgfALpS9Tvgr/y1ki7ubY4AEGJeWL3teakrnmOacN4XGiyD00lkzEHgqCQ71wGxoi/zb7Y9a4orOtvMV3/Jw=='; - $salt = '56dFqW+kswqktw=='; - $saltSeparator = 'Bw=='; - $signerKey = 'XyEKE9RcTDeLEsL/RjwPDBv/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ=='; - - $options = [ 'salt' => $salt, 'saltSeparator' => $saltSeparator, 'signerKey' => $signerKey ]; - $generatedHash = Auth::passwordHash($plain, 'scryptMod', $options); - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'scryptMod', $options)); - $this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'scryptMod', $options)); - $this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'scryptMod', $options)); - } - - public function testUnknownAlgo() - { - $this->expectExceptionMessage('Hashing algorithm \'md8\' is not supported.'); - - // Bcrypt - Cost 5 - $plain = 'whatIsMd8?!?'; - $generatedHash = Auth::passwordHash($plain, 'md8'); - $this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md8')); - } - - public function testPasswordGenerator(): void - { - $this->assertEquals(\mb_strlen(Auth::passwordGenerator()), 40); - $this->assertEquals(\mb_strlen(Auth::passwordGenerator(5)), 10); - } - - public function testTokenGenerator(): void - { - $this->assertEquals(\strlen(Auth::tokenGenerator()), 256); - $this->assertEquals(\strlen(Auth::tokenGenerator(5)), 5); - } - - public function testCodeGenerator(): void - { - $this->assertEquals(6, \strlen(Auth::codeGenerator())); - $this->assertEquals(\mb_strlen(Auth::codeGenerator(256)), 256); - $this->assertEquals(\mb_strlen(Auth::codeGenerator(10)), 10); - $this->assertTrue(is_numeric(Auth::codeGenerator(5))); - } - - public function testSessionVerify(): void - { - $expireTime1 = 60 * 60 * 24; - - $secret = 'secret1'; - $hash = Auth::hash($secret); - $tokens1 = [ - new Document([ - '$id' => ID::custom('token1'), - 'secret' => $hash, - 'provider' => Auth::SESSION_PROVIDER_EMAIL, - 'providerUid' => 'test@example.com', - 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1), - ]), - new Document([ - '$id' => ID::custom('token2'), - 'secret' => 'secret2', - 'provider' => Auth::SESSION_PROVIDER_EMAIL, - 'providerUid' => 'test@example.com', - 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1), - ]), - ]; - - $expireTime2 = -60 * 60 * 24; - - $tokens2 = [ - new Document([ // Correct secret and type time, wrong expire time - '$id' => ID::custom('token1'), - 'secret' => $hash, - 'provider' => Auth::SESSION_PROVIDER_EMAIL, - 'providerUid' => 'test@example.com', - 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2), - ]), - new Document([ - '$id' => ID::custom('token2'), - 'secret' => 'secret2', - 'provider' => Auth::SESSION_PROVIDER_EMAIL, - 'providerUid' => 'test@example.com', - 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2), - ]), - ]; - - $this->assertEquals(Auth::sessionVerify($tokens1, $secret), 'token1'); - $this->assertEquals(Auth::sessionVerify($tokens1, 'false-secret'), false); - $this->assertEquals(Auth::sessionVerify($tokens2, $secret), false); - $this->assertEquals(Auth::sessionVerify($tokens2, 'false-secret'), false); - } - - public function testTokenVerify(): void - { - $secret = 'secret1'; - $hash = Auth::hash($secret); - $tokens1 = [ - new Document([ - '$id' => ID::custom('token1'), - 'type' => Auth::TOKEN_TYPE_RECOVERY, - 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)), - 'secret' => $hash, - ]), - new Document([ - '$id' => ID::custom('token2'), - 'type' => Auth::TOKEN_TYPE_RECOVERY, - 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)), - 'secret' => 'secret2', - ]), - ]; - - $tokens2 = [ - new Document([ // Correct secret and type time, wrong expire time - '$id' => ID::custom('token1'), - 'type' => Auth::TOKEN_TYPE_RECOVERY, - 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)), - 'secret' => $hash, - ]), - new Document([ - '$id' => ID::custom('token2'), - 'type' => Auth::TOKEN_TYPE_RECOVERY, - 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)), - 'secret' => 'secret2', - ]), - ]; - - $tokens3 = [ // Correct secret and expire time, wrong type - new Document([ - '$id' => ID::custom('token1'), - 'type' => Auth::TOKEN_TYPE_INVITE, - 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)), - 'secret' => $hash, - ]), - new Document([ - '$id' => ID::custom('token2'), - 'type' => Auth::TOKEN_TYPE_RECOVERY, - 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)), - 'secret' => 'secret2', - ]), - ]; - - $this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, $secret), $tokens1[0]); - $this->assertEquals(Auth::tokenVerify($tokens1, null, $secret), $tokens1[0]); - $this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false); - $this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, $secret), false); - $this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false); - $this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_RECOVERY, $secret), false); - $this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false); - } - - public function testIsPrivilegedUser(): void - { - $this->assertEquals(false, Auth::isPrivilegedUser([])); - $this->assertEquals(false, Auth::isPrivilegedUser([Role::guests()->toString()])); - $this->assertEquals(false, Auth::isPrivilegedUser([Role::users()->toString()])); - $this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_ADMIN])); - $this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_DEVELOPER])); - $this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER])); - $this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS])); - $this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_SYSTEM])); - - $this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_APPS])); - $this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS, Role::guests()->toString()])); - $this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER, Role::guests()->toString()])); - $this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_ADMIN, Auth::USER_ROLE_DEVELOPER])); - } - - public function testIsAppUser(): void - { - $this->assertEquals(false, Auth::isAppUser([])); - $this->assertEquals(false, Auth::isAppUser([Role::guests()->toString()])); - $this->assertEquals(false, Auth::isAppUser([Role::users()->toString()])); - $this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_ADMIN])); - $this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_DEVELOPER])); - $this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER])); - $this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS])); - $this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_SYSTEM])); - - $this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_APPS])); - $this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS, Role::guests()->toString()])); - $this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER, Role::guests()->toString()])); - $this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_ADMIN, Auth::USER_ROLE_DEVELOPER])); - } - - public function testGuestRoles(): void - { - $user = new Document([ - '$id' => '' - ]); - - $roles = Auth::getRoles($user); - $this->assertCount(1, $roles); - $this->assertContains(Role::guests()->toString(), $roles); - } - - public function testUserRoles(): void - { - $user = new Document([ - '$id' => ID::custom('123'), - 'labels' => [ - 'vip', - 'admin' - ], - 'emailVerification' => true, - 'phoneVerification' => true, - 'memberships' => [ - [ - '$id' => ID::custom('456'), - 'teamId' => ID::custom('abc'), - 'confirm' => true, - 'roles' => [ - 'administrator', - 'moderator' - ] - ], - [ - '$id' => ID::custom('abc'), - 'teamId' => ID::custom('def'), - 'confirm' => true, - 'roles' => [ - 'guest' - ] - ] - ] - ]); - - $roles = Auth::getRoles($user); - - $this->assertCount(13, $roles); - $this->assertContains(Role::users()->toString(), $roles); - $this->assertContains(Role::user(ID::custom('123'))->toString(), $roles); - $this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles); - $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles); - $this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles); - $this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles); - $this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles); - $this->assertContains(Role::team(ID::custom('def'))->toString(), $roles); - $this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles); - $this->assertContains(Role::member(ID::custom('456'))->toString(), $roles); - $this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles); - $this->assertContains('label:vip', $roles); - $this->assertContains('label:admin', $roles); - - // Disable all verification - $user['emailVerification'] = false; - $user['phoneVerification'] = false; - - $roles = Auth::getRoles($user); - $this->assertContains(Role::users(Roles::DIMENSION_UNVERIFIED)->toString(), $roles); - $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_UNVERIFIED)->toString(), $roles); - - // Enable single verification type - $user['emailVerification'] = true; - - $roles = Auth::getRoles($user); - $this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles); - $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles); - } - - public function testPrivilegedUserRoles(): void - { - Authorization::setRole(Auth::USER_ROLE_OWNER); - $user = new Document([ - '$id' => ID::custom('123'), - 'emailVerification' => true, - 'phoneVerification' => true, - 'memberships' => [ - [ - '$id' => ID::custom('def'), - 'teamId' => ID::custom('abc'), - 'confirm' => true, - 'roles' => [ - 'administrator', - 'moderator' - ] - ], - [ - '$id' => ID::custom('abc'), - 'teamId' => ID::custom('def'), - 'confirm' => true, - 'roles' => [ - 'guest' - ] - ] - ] - ]); - - $roles = Auth::getRoles($user); - - $this->assertCount(7, $roles); - $this->assertNotContains(Role::users()->toString(), $roles); - $this->assertNotContains(Role::user(ID::custom('123'))->toString(), $roles); - $this->assertNotContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles); - $this->assertNotContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles); - $this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles); - $this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles); - $this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles); - $this->assertContains(Role::team(ID::custom('def'))->toString(), $roles); - $this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles); - $this->assertContains(Role::member(ID::custom('def'))->toString(), $roles); - $this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles); - } - - public function testAppUserRoles(): void - { - Authorization::setRole(Auth::USER_ROLE_APPS); - $user = new Document([ - '$id' => ID::custom('123'), - 'memberships' => [ - [ - '$id' => ID::custom('def'), - 'teamId' => ID::custom('abc'), - 'confirm' => true, - 'roles' => [ - 'administrator', - 'moderator' - ] - ], - [ - '$id' => ID::custom('abc'), - 'teamId' => ID::custom('def'), - 'confirm' => true, - 'roles' => [ - 'guest' - ] - ] - ] - ]); - - $roles = Auth::getRoles($user); - - $this->assertCount(7, $roles); - $this->assertNotContains(Role::users()->toString(), $roles); - $this->assertNotContains(Role::user(ID::custom('123'))->toString(), $roles); - $this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles); - $this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles); - $this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles); - $this->assertContains(Role::team(ID::custom('def'))->toString(), $roles); - $this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles); - $this->assertContains(Role::member(ID::custom('def'))->toString(), $roles); - $this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles); - } -} diff --git a/tests/unit/Auth/KeyTest.php b/tests/unit/Auth/KeyTest.php index 8ae2114697..920608e82f 100644 --- a/tests/unit/Auth/KeyTest.php +++ b/tests/unit/Auth/KeyTest.php @@ -3,8 +3,8 @@ namespace Tests\Unit\Auth; use Ahc\Jwt\JWT; -use Appwrite\Auth\Auth; use Appwrite\Auth\Key; +use Appwrite\Utopia\Database\Documents\User; use PHPUnit\Framework\TestCase; use Utopia\Config\Config; use Utopia\Database\Document; @@ -21,7 +21,7 @@ class KeyTest extends TestCase 'collections.read', 'documents.read', ]; - $roleScopes = Config::getParam('roles', [])[Auth::USER_ROLE_APPS]['scopes']; + $roleScopes = Config::getParam('roles', [])[User::ROLE_APPS]['scopes']; $key = static::generateKey($projectId, $usage, $scopes); $project = new Document(['$id' => $projectId,]); @@ -29,7 +29,7 @@ class KeyTest extends TestCase $this->assertEquals($projectId, $decoded->getProjectId()); $this->assertEquals(API_KEY_DYNAMIC, $decoded->getType()); - $this->assertEquals(Auth::USER_ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); } diff --git a/tests/unit/Messaging/MessagingChannelsTest.php b/tests/unit/Messaging/MessagingChannelsTest.php index 8ba0374093..42e433568f 100644 --- a/tests/unit/Messaging/MessagingChannelsTest.php +++ b/tests/unit/Messaging/MessagingChannelsTest.php @@ -2,10 +2,9 @@ namespace Tests\Unit\Messaging; -use Appwrite\Auth\Auth; use Appwrite\Messaging\Adapter\Realtime; +use Appwrite\Utopia\Database\Documents\User; use PHPUnit\Framework\TestCase; -use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; @@ -50,7 +49,7 @@ class MessagingChannelsTest extends TestCase */ for ($i = 0; $i < $this->connectionsPerChannel; $i++) { foreach ($this->allChannels as $index => $channel) { - $user = new Document([ + $user = new User([ '$id' => ID::custom('user' . $this->connectionsCount), 'memberships' => [ [ @@ -59,14 +58,14 @@ class MessagingChannelsTest extends TestCase 'confirm' => true, 'roles' => [ empty($index % 2) - ? Auth::USER_ROLE_ADMIN + ? User::ROLE_ADMIN : 'member', ] ] ] ]); - $roles = Auth::getRoles($user); + $roles = $user->getRoles(); $parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId()); @@ -86,11 +85,11 @@ class MessagingChannelsTest extends TestCase */ for ($i = 0; $i < $this->connectionsPerChannel; $i++) { foreach ($this->allChannels as $index => $channel) { - $user = new Document([ + $user = new User([ '$id' => '' ]); - $roles = Auth::getRoles($user); + $roles = $user->getRoles(); $parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId()); @@ -294,7 +293,7 @@ class MessagingChannelsTest extends TestCase } $role = empty($index % 2) - ? Auth::USER_ROLE_ADMIN + ? User::ROLE_ADMIN : 'member'; $permissions = [ diff --git a/tests/unit/Utopia/Database/Documents/UserTest.php b/tests/unit/Utopia/Database/Documents/UserTest.php new file mode 100644 index 0000000000..4675e8d73f --- /dev/null +++ b/tests/unit/Utopia/Database/Documents/UserTest.php @@ -0,0 +1,352 @@ +toString()); + } + + public function testSessionVerify(): void + { + $proofForToken = new Token(); + $expireTime1 = 60 * 60 * 24; + + $secret = 'secret1'; + $hash = $proofForToken->hash($secret); + $tokens1 = [ + new Document([ + '$id' => ID::custom('token1'), + 'secret' => $hash, + 'provider' => SESSION_PROVIDER_EMAIL, + 'providerUid' => 'test@example.com', + 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1), + ]), + new Document([ + '$id' => ID::custom('token2'), + 'secret' => 'secret2', + 'provider' => SESSION_PROVIDER_EMAIL, + 'providerUid' => 'test@example.com', + 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1), + ]), + ]; + + $expireTime2 = -60 * 60 * 24; + + $tokens2 = [ + new Document([ // Correct secret and type time, wrong expire time + '$id' => ID::custom('token1'), + 'secret' => $hash, + 'provider' => SESSION_PROVIDER_EMAIL, + 'providerUid' => 'test@example.com', + 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2), + ]), + new Document([ + '$id' => ID::custom('token2'), + 'secret' => 'secret2', + 'provider' => SESSION_PROVIDER_EMAIL, + 'providerUid' => 'test@example.com', + 'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2), + ]), + ]; + + $user1 = new User([ + '$id' => ID::custom('user1'), + 'sessions' => $tokens1, + + ]); + + $user2 = new User([ + '$id' => ID::custom('user2'), + 'sessions' => $tokens2, + ]); + + $this->assertEquals('token1', $user1->sessionVerify($secret, $proofForToken)); + $this->assertEquals($user1->sessionVerify('false-secret', $proofForToken), false); + $this->assertEquals($user2->sessionVerify($secret, $proofForToken), false); + $this->assertEquals($user2->sessionVerify('false-secret', $proofForToken), false); + } + + public function testTokenVerify(): void + { + $proofForToken = new Token(); + $secret = 'secret1'; + $hash = $proofForToken->hash($secret); + $tokens1 = [ + new Document([ + '$id' => ID::custom('token1'), + 'type' => TOKEN_TYPE_RECOVERY, + 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)), + 'secret' => $hash, + ]), + new Document([ + '$id' => ID::custom('token2'), + 'type' => TOKEN_TYPE_RECOVERY, + 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)), + 'secret' => 'secret2', + ]), + ]; + + $tokens2 = [ + new Document([ // Correct secret and type time, wrong expire time + '$id' => ID::custom('token1'), + 'type' => TOKEN_TYPE_RECOVERY, + 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)), + 'secret' => $hash, + ]), + new Document([ + '$id' => ID::custom('token2'), + 'type' => TOKEN_TYPE_RECOVERY, + 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)), + 'secret' => 'secret2', + ]), + ]; + + $tokens3 = [ // Correct secret and expire time, wrong type + new Document([ + '$id' => ID::custom('token1'), + 'type' => TOKEN_TYPE_INVITE, + 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)), + 'secret' => $hash, + ]), + new Document([ + '$id' => ID::custom('token2'), + 'type' => TOKEN_TYPE_RECOVERY, + 'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)), + 'secret' => 'secret2', + ]), + ]; + + $user1 = new User([ + '$id' => ID::custom('user1'), + 'tokens' => $tokens1, + ]); + + $user2 = new User([ + '$id' => ID::custom('user2'), + 'tokens' => $tokens2, + ]); + + $user3 = new User([ + '$id' => ID::custom('user3'), + 'tokens' => $tokens3, + ]); + + $this->assertEquals($user1->tokenVerify(TOKEN_TYPE_RECOVERY, $secret, $proofForToken), $tokens1[0]); + $this->assertEquals($user1->tokenVerify(null, $secret, $proofForToken), $tokens1[0]); + $this->assertEquals($user1->tokenVerify(TOKEN_TYPE_RECOVERY, 'false-secret', $proofForToken), false); + $this->assertEquals($user2->tokenVerify(TOKEN_TYPE_RECOVERY, $secret, $proofForToken), false); + $this->assertEquals($user2->tokenVerify(TOKEN_TYPE_RECOVERY, 'false-secret', $proofForToken), false); + $this->assertEquals($user3->tokenVerify(TOKEN_TYPE_RECOVERY, $secret, $proofForToken), false); + $this->assertEquals($user3->tokenVerify(TOKEN_TYPE_RECOVERY, 'false-secret', $proofForToken), false); + } + + public function testIsPrivilegedUser(): void + { + $this->assertEquals(false, User::isPrivileged([])); + $this->assertEquals(false, User::isPrivileged([Role::guests()->toString()])); + $this->assertEquals(false, User::isPrivileged([Role::users()->toString()])); + $this->assertEquals(true, User::isPrivileged([User::ROLE_ADMIN])); + $this->assertEquals(true, User::isPrivileged([User::ROLE_DEVELOPER])); + $this->assertEquals(true, User::isPrivileged([User::ROLE_OWNER])); + $this->assertEquals(false, User::isPrivileged([User::ROLE_APPS])); + $this->assertEquals(false, User::isPrivileged([User::ROLE_SYSTEM])); + + $this->assertEquals(false, User::isPrivileged([User::ROLE_APPS, User::ROLE_APPS])); + $this->assertEquals(false, User::isPrivileged([User::ROLE_APPS, Role::guests()->toString()])); + $this->assertEquals(true, User::isPrivileged([User::ROLE_OWNER, Role::guests()->toString()])); + $this->assertEquals(true, User::isPrivileged([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER])); + } + + public function testIsAppUser(): void + { + $this->assertEquals(false, User::isApp([])); + $this->assertEquals(false, User::isApp([Role::guests()->toString()])); + $this->assertEquals(false, User::isApp([Role::users()->toString()])); + $this->assertEquals(false, User::isApp([User::ROLE_ADMIN])); + $this->assertEquals(false, User::isApp([User::ROLE_DEVELOPER])); + $this->assertEquals(false, User::isApp([User::ROLE_OWNER])); + $this->assertEquals(true, User::isApp([User::ROLE_APPS])); + $this->assertEquals(false, User::isApp([User::ROLE_SYSTEM])); + + $this->assertEquals(true, User::isApp([User::ROLE_APPS, User::ROLE_APPS])); + $this->assertEquals(true, User::isApp([User::ROLE_APPS, Role::guests()->toString()])); + $this->assertEquals(false, User::isApp([User::ROLE_OWNER, Role::guests()->toString()])); + $this->assertEquals(false, User::isApp([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER])); + } + + public function testGuestRoles(): void + { + $user = new User([ + '$id' => '' + ]); + + $roles = $user->getRoles(); + $this->assertCount(1, $roles); + $this->assertContains(Role::guests()->toString(), $roles); + } + + public function testUserRoles(): void + { + $user = new User([ + '$id' => ID::custom('123'), + 'labels' => [ + 'vip', + 'admin' + ], + 'emailVerification' => true, + 'phoneVerification' => true, + 'memberships' => [ + [ + '$id' => ID::custom('456'), + 'teamId' => ID::custom('abc'), + 'confirm' => true, + 'roles' => [ + 'administrator', + 'moderator' + ] + ], + [ + '$id' => ID::custom('abc'), + 'teamId' => ID::custom('def'), + 'confirm' => true, + 'roles' => [ + 'guest' + ] + ] + ] + ]); + + $roles = $user->getRoles(); + + $this->assertCount(13, $roles); + $this->assertContains(Role::users()->toString(), $roles); + $this->assertContains(Role::user(ID::custom('123'))->toString(), $roles); + $this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles); + $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles); + $this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles); + $this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles); + $this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles); + $this->assertContains(Role::team(ID::custom('def'))->toString(), $roles); + $this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles); + $this->assertContains(Role::member(ID::custom('456'))->toString(), $roles); + $this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles); + $this->assertContains('label:vip', $roles); + $this->assertContains('label:admin', $roles); + + // Disable all verification + $user['emailVerification'] = false; + $user['phoneVerification'] = false; + + $roles = $user->getRoles(); + $this->assertContains(Role::users(Roles::DIMENSION_UNVERIFIED)->toString(), $roles); + $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_UNVERIFIED)->toString(), $roles); + + // Enable single verification type + $user['emailVerification'] = true; + + $roles = $user->getRoles(); + $this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles); + $this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles); + } + + public function testPrivilegedUserRoles(): void + { + Authorization::setRole(User::ROLE_OWNER); + $user = new User([ + '$id' => ID::custom('123'), + 'emailVerification' => true, + 'phoneVerification' => true, + 'memberships' => [ + [ + '$id' => ID::custom('def'), + 'teamId' => ID::custom('abc'), + 'confirm' => true, + 'roles' => [ + 'administrator', + 'moderator' + ] + ], + [ + '$id' => ID::custom('abc'), + 'teamId' => ID::custom('def'), + 'confirm' => true, + 'roles' => [ + 'guest' + ] + ] + ] + ]); + + $roles = $user->getRoles(); + + $this->assertCount(7, $roles); + $this->assertNotContains(Role::users()->toString(), $roles); + $this->assertNotContains(Role::user(ID::custom('123'))->toString(), $roles); + $this->assertNotContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles); + $this->assertNotContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles); + $this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles); + $this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles); + $this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles); + $this->assertContains(Role::team(ID::custom('def'))->toString(), $roles); + $this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles); + $this->assertContains(Role::member(ID::custom('def'))->toString(), $roles); + $this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles); + } + + public function testAppUserRoles(): void + { + Authorization::setRole(User::ROLE_APPS); + $user = new User([ + '$id' => ID::custom('123'), + 'memberships' => [ + [ + '$id' => ID::custom('def'), + 'teamId' => ID::custom('abc'), + 'confirm' => true, + 'roles' => [ + 'administrator', + 'moderator' + ] + ], + [ + '$id' => ID::custom('abc'), + 'teamId' => ID::custom('def'), + 'confirm' => true, + 'roles' => [ + 'guest' + ] + ] + ] + ]); + + $roles = $user->getRoles(); + + $this->assertCount(7, $roles); + $this->assertNotContains(Role::users()->toString(), $roles); + $this->assertNotContains(Role::user(ID::custom('123'))->toString(), $roles); + $this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles); + $this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles); + $this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles); + $this->assertContains(Role::team(ID::custom('def'))->toString(), $roles); + $this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles); + $this->assertContains(Role::member(ID::custom('def'))->toString(), $roles); + $this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles); + } +}