diff --git a/.env b/.env index 351447a8b4..2c9e740c8b 100644 --- a/.env +++ b/.env @@ -56,8 +56,8 @@ _APP_SMTP_PORT=1025 _APP_SMTP_SECURE= _APP_SMTP_USERNAME= _APP_SMTP_PASSWORD= -_APP_PHONE_PROVIDER=phone://mock -_APP_PHONE_FROM=+123456789 +_APP_SMS_PROVIDER=sms://mock +_APP_SMS_FROM=+123456789 _APP_STORAGE_LIMIT=30000000 _APP_STORAGE_PREVIEW_LIMIT=20000000 _APP_FUNCTIONS_SIZE_LIMIT=30000000 @@ -72,6 +72,7 @@ OPEN_RUNTIMES_NETWORK=appwrite_runtimes _APP_EXECUTOR_SECRET=your-secret-key _APP_EXECUTOR_HOST=http://appwrite-executor/v1 _APP_MAINTENANCE_INTERVAL=86400 +_APP_MAINTENANCE_RETENTION_CACHE=2592000 _APP_MAINTENANCE_RETENTION_EXECUTION=1209600 _APP_MAINTENANCE_RETENTION_ABUSE=86400 _APP_MAINTENANCE_RETENTION_AUDIT=1209600 diff --git a/Dockerfile b/Dockerfile index 29d495ec26..4930bdcb87 100755 --- a/Dockerfile +++ b/Dockerfile @@ -123,6 +123,33 @@ RUN \ ./configure && \ make && make install +# Rust Extensions Compile Image +FROM php:8.0.18-cli as rust_compile + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + +ENV PATH=/root/.cargo/bin:$PATH + +RUN apt-get update && apt-get install musl-tools build-essential clang-11 git -y +RUN rustup target add $(uname -m)-unknown-linux-musl + +# Install ZigBuild for easier cross-compilation +RUN curl https://ziglang.org/builds/zig-linux-$(uname -m)-0.10.0-dev.2674+d980c6a38.tar.xz --output /tmp/zig.tar.xz +RUN tar -xf /tmp/zig.tar.xz -C /tmp/ && cp -r /tmp/zig-linux-$(uname -m)-0.10.0-dev.2674+d980c6a38 /tmp/zig/ +ENV PATH=/tmp/zig:$PATH +RUN cargo install cargo-zigbuild +ENV RUSTFLAGS="-C target-feature=-crt-static" + +FROM rust_compile as scrypt + +WORKDIR /usr/local/lib/php/extensions/ + +RUN \ + git clone --depth 1 https://github.com/appwrite/php-scrypt.git && \ + cd php-scrypt && \ + cargo zigbuild --workspace --all-targets --target $(uname -m)-unknown-linux-musl --release && \ + mv target/$(uname -m)-unknown-linux-musl/release/libphp_scrypt.so target/libphp_scrypt.so + FROM php:8.0.18-cli-alpine3.15 as final LABEL maintainer="team@appwrite.io" @@ -193,8 +220,8 @@ ENV _APP_SERVER=swoole \ _APP_SMTP_SECURE= \ _APP_SMTP_USERNAME= \ _APP_SMTP_PASSWORD= \ - _APP_PHONE_PROVIDER= \ - _APP_PHONE_FROM= \ + _APP_SMS_PROVIDER= \ + _APP_SMS_FROM= \ _APP_FUNCTIONS_SIZE_LIMIT=30000000 \ _APP_FUNCTIONS_TIMEOUT=900 \ _APP_FUNCTIONS_CONTAINERS=10 \ @@ -263,6 +290,7 @@ COPY --from=imagick /usr/local/lib/php/extensions/no-debug-non-zts-20200930/imag COPY --from=yaml /usr/local/lib/php/extensions/no-debug-non-zts-20200930/yaml.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/ COPY --from=maxmind /usr/local/lib/php/extensions/no-debug-non-zts-20200930/maxminddb.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/ COPY --from=mongodb /usr/local/lib/php/extensions/no-debug-non-zts-20200930/mongodb.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/ +COPY --from=scrypt /usr/local/lib/php/extensions/php-scrypt/target/libphp_scrypt.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/ # Add Source Code COPY ./app /usr/src/code/app @@ -319,6 +347,7 @@ RUN echo extension=redis.so >> /usr/local/etc/php/conf.d/redis.ini RUN echo extension=imagick.so >> /usr/local/etc/php/conf.d/imagick.ini RUN echo extension=yaml.so >> /usr/local/etc/php/conf.d/yaml.ini RUN echo extension=maxminddb.so >> /usr/local/etc/php/conf.d/maxminddb.ini +RUN echo extension=libphp_scrypt.so >> /usr/local/etc/php/conf.d/libphp_scrypt.ini RUN if [ "$DEBUG" == "true" ]; then printf "zend_extension=yasd \nyasd.debug_mode=remote \nyasd.init_file=/usr/local/dev/yasd_init.php \nyasd.remote_port=9005 \nyasd.log_level=-1" >> /usr/local/etc/php/conf.d/yasd.ini; fi RUN if [ "$DEBUG" == "true" ]; then echo "opcache.enable=0" >> /usr/local/etc/php/conf.d/appwrite.ini; fi diff --git a/app/config/collections.php b/app/config/collections.php index d3ea600574..5e71a3bf52 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1,5 +1,6 @@ false, 'default' => null, 'array' => false, + 'filters' => ['encrypt'], + ], + [ + '$id' => 'hash', // Hashing algorithm used to hash the password + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 256, + 'signed' => true, + 'required' => false, + 'default' => Auth::DEFAULT_ALGO, + 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('hashOptions'), // Configuration of hashing algorithm + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 65535, + 'signed' => true, + 'required' => false, + 'default' => Auth::DEFAULT_ALGO_OPTIONS, + 'array' => false, + 'filters' => ['json'], + ], [ '$id' => ID::custom('passwordUpdate'), 'type' => Database::VAR_DATETIME, @@ -2388,6 +2411,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('stdout'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 1000000, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('statusCode'), 'type' => Database::VAR_INTEGER, @@ -2766,6 +2800,62 @@ $collections = [ ], ] ], + 'cache' => [ + '$collection' => Database::METADATA, + '$id' => 'cache', + 'name' => 'Cache', + 'attributes' => [ + [ + '$id' => 'resource', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 255, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'accessedAt', + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'signature', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 255, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + ], + 'indexes' => [ + [ + '$id' => '_key_accessedAt', + 'type' => Database::INDEX_KEY, + 'attributes' => ['accessedAt'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => '_key_resource', + 'type' => Database::INDEX_KEY, + 'attributes' => ['resource'], + 'lengths' => [], + 'orders' => [], + ], + ], + ], 'files' => [ '$collection' => ID::custom('buckets'), '$id' => ID::custom('files'), diff --git a/app/config/errors.php b/app/config/errors.php index ee60a6ee54..098622ce24 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -50,7 +50,7 @@ return [ ], Exception::GENERAL_PHONE_DISABLED => [ 'name' => Exception::GENERAL_PHONE_DISABLED, - 'description' => 'Phone provider is not configured. Please check the _APP_PHONE_PROVIDER environment variable of your Appwrite server.', + 'description' => 'Phone provider is not configured. Please check the _APP_SMS_PROVIDER environment variable of your Appwrite server.', 'code' => 503, ], Exception::GENERAL_ARGUMENT_INVALID => [ diff --git a/app/config/runtimes.php b/app/config/runtimes.php index 03be3f07fc..c24aaa109e 100644 --- a/app/config/runtimes.php +++ b/app/config/runtimes.php @@ -7,7 +7,7 @@ use Utopia\App; use Appwrite\Runtimes\Runtimes; -$runtimes = new Runtimes('v1'); +$runtimes = new Runtimes('v2'); $allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES')); diff --git a/app/config/variables.php b/app/config/variables.php index 7b4c4b4ace..9d347cd1cf 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -394,8 +394,8 @@ return [ 'description' => '', 'variables' => [ [ - 'name' => '_APP_PHONE_PROVIDER', - 'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'phone://[USER]:[SECRET]@[PROVIDER]'. \n\nAvailable providers are twilio, text-magic and telesign.", + 'name' => '_APP_SMS_PROVIDER', + 'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'. \n\nAvailable providers are twilio, text-magic and telesign.", 'introduction' => '0.15.0', 'default' => '', 'required' => false, @@ -403,7 +403,7 @@ return [ 'filter' => '' ], [ - 'name' => '_APP_PHONE_FROM', + 'name' => '_APP_SMS_FROM', 'description' => 'Phone number used for sending out messages. Must start with a leading \'+\' and maximum of 15 digits without spaces (+123456789).', 'introduction' => '0.15.0', 'default' => '', @@ -804,6 +804,15 @@ return [ 'question' => '', 'filter' => '' ], + [ + 'name' => '_APP_MAINTENANCE_RETENTION_CACHE', + 'description' => 'The maximum duration (in seconds) upto which to retain cached files. The default value is 2592000 seconds (30 days).', + 'introduction' => '0.16.0', + 'default' => '2592000', + 'required' => false, + 'question' => '', + 'filter' => '' + ], [ 'name' => '_APP_MAINTENANCE_RETENTION_EXECUTION', 'description' => 'The maximum duration (in seconds) upto which to retain execution logs. The default value is 1209600 seconds (14 days).', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 13a9867447..1458203d3a 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2,9 +2,9 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; -use Appwrite\Auth\Phone; +use Appwrite\SMS\Adapter\Mock; use Appwrite\Auth\Validator\Password; -use Appwrite\Auth\Validator\Phone as ValidatorPhone; +use Appwrite\Auth\Validator\Phone; use Appwrite\Detector\Detector; use Appwrite\Event\Audit; use Appwrite\Event\Event; @@ -51,13 +51,15 @@ App::post('/v1/account') ->label('event', 'users.[userId].create') ->label('scope', 'public') ->label('auth.type', 'emailPassword') + ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') ->label('sdk.auth', []) ->label('sdk.namespace', 'account') ->label('sdk.method', 'create') ->label('sdk.description', '/docs/references/account/create.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_USER) + ->label('sdk.response.model', Response::MODEL_ACCOUNT) ->label('abuse-limit', 10) ->param('userId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('email', '', new Email(), 'User email.') @@ -67,10 +69,9 @@ App::post('/v1/account') ->inject('response') ->inject('project') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $project, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { + ->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $project, Database $dbForProject, Stats $usage, Event $events) { $email = \strtolower($email); if ('console' === $project->getId()) { @@ -108,7 +109,9 @@ App::post('/v1/account') 'email' => $email, 'emailVerification' => false, 'status' => true, - 'password' => Auth::passwordHash($password), + 'password' => Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS), + 'hash' => Auth::DEFAULT_ALGO, + 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, 'passwordUpdate' => DateTime::now(), 'registration' => DateTime::now(), 'reset' => false, @@ -127,16 +130,11 @@ App::post('/v1/account') Authorization::setRole(Role::user($user->getId())->toString()); Authorization::setRole(Role::users()->toString()); - $audits - ->setResource('user/' . $user->getId()) - ->setUser($user) - ; - $usage->setParam('users.create', 1); $events->setParam('userId', $user->getId()); $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_ACCOUNT); }); App::post('/v1/account/sessions/email') @@ -146,6 +144,8 @@ App::post('/v1/account/sessions/email') ->label('event', 'users.[userId].sessions.[sessionId].create') ->label('scope', 'public') ->label('auth.type', 'emailPassword') + ->label('audits.resource', 'user/{response.userId}') + ->label('audits.userId', '{response.userId}') ->label('sdk.auth', []) ->label('sdk.namespace', 'account') ->label('sdk.method', 'createEmailSession') @@ -162,10 +162,9 @@ App::post('/v1/account/sessions/email') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $email, string $password, Request $request, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audits, Stats $usage, Event $events) { + ->action(function (string $email, string $password, Request $request, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Stats $usage, Event $events) { $email = \strtolower($email); $protocol = $request->getProtocol(); @@ -174,8 +173,8 @@ App::post('/v1/account/sessions/email') Query::equal('email', [$email]), ]); - if (!$profile || !Auth::passwordVerify($password, $profile->getAttribute('password'))) { - throw new Exception(Exception::USER_INVALID_CREDENTIALS); // Wrong password or username + if (!$profile || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) { + throw new Exception(Exception::USER_INVALID_CREDENTIALS); } if (false === $profile->getAttribute('status')) { // Account is blocked @@ -206,18 +205,23 @@ App::post('/v1/account/sessions/email') Authorization::setRole(Role::user($profile->getId())->toString()); + // Re-hash if not using recommended algo + if ($profile->getAttribute('hash') !== Auth::DEFAULT_ALGO) { + $profile + ->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS)) + ->setAttribute('hash', Auth::DEFAULT_ALGO) + ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS); + $dbForProject->updateDocument('users', $profile->getId(), $profile); + } + + $dbForProject->deleteCachedDocument('users', $profile->getId()); + $session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [ Permission::read(Role::user($profile->getId())), Permission::update(Role::user($profile->getId())), Permission::delete(Role::user($profile->getId())), ])); - $dbForProject->deleteCachedDocument('users', $profile->getId()); - - $audits - ->setResource('user/' . $profile->getId()) - ->setUser($profile) - ; if (!Config::getParam('domainVerification')) { $response @@ -366,6 +370,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->label('error', __DIR__ . '/../../views/general/error.phtml') ->label('event', 'users.[userId].sessions.[sessionId].create') ->label('scope', 'public') + ->label('audits.resource', 'user/{user.$id}') ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') ->label('docs', false) @@ -378,10 +383,9 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->inject('user') ->inject('dbForProject') ->inject('geodb') - ->inject('audits') ->inject('events') ->inject('usage') - ->action(function (string $provider, string $code, string $state, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Audit $audits, Event $events, Stats $usage) use ($oauthDefaultSuccess) { + ->action(function (string $provider, string $code, string $state, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $events, Stats $usage) use ($oauthDefaultSuccess) { $protocol = $request->getProtocol(); $callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId(); @@ -497,7 +501,9 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'email' => $email, 'emailVerification' => true, 'status' => true, // Email should already be authenticated by OAuth2 provider - 'password' => Auth::passwordHash(Auth::passwordGenerator()), + 'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS), + 'hash' => Auth::DEFAULT_ALGO, + 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, 'passwordUpdate' => null, 'registration' => DateTime::now(), 'reset' => false, @@ -565,11 +571,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $dbForProject->deleteCachedDocument('users', $user->getId()); - $audits - ->setResource('user/' . $user->getId()) - ->setUser($user) - ; - $usage ->setParam('users.sessions.create', 1) ->setParam('projectId', $project->getId()) @@ -607,12 +608,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ; }); - App::post('/v1/account/sessions/magic-url') ->desc('Create Magic URL session') ->groups(['api', 'account']) ->label('scope', 'public') ->label('auth.type', 'magic-url') + ->label('audits.resource', 'user/{response.userId}') + ->label('audits.userId', '{response.userId}') ->label('sdk.auth', []) ->label('sdk.namespace', 'account') ->label('sdk.method', 'createMagicURLSession') @@ -630,10 +632,9 @@ App::post('/v1/account/sessions/magic-url') ->inject('project') ->inject('dbForProject') ->inject('locale') - ->inject('audits') ->inject('events') ->inject('mails') - ->action(function (string $userId, string $email, string $url, Request $request, Response $response, Document $project, Database $dbForProject, Locale $locale, Audit $audits, Event $events, Mail $mails) { + ->action(function (string $userId, string $email, string $url, Request $request, Response $response, Document $project, Database $dbForProject, Locale $locale, Event $events, Mail $mails) { if (empty(App::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled'); @@ -669,6 +670,8 @@ App::post('/v1/account/sessions/magic-url') 'emailVerification' => false, 'status' => true, 'password' => null, + 'hash' => Auth::DEFAULT_ALGO, + 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, 'passwordUpdate' => null, 'registration' => DateTime::now(), 'reset' => false, @@ -731,11 +734,6 @@ App::post('/v1/account/sessions/magic-url') // Hide secret for clients $token->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $loginSecret : ''); - $audits - ->setResource('user/' . $user->getId()) - ->setUser($user) - ; - $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($token, Response::MODEL_TOKEN) @@ -747,6 +745,8 @@ App::put('/v1/account/sessions/magic-url') ->groups(['api', 'account']) ->label('scope', 'public') ->label('event', 'users.[userId].sessions.[sessionId].create') + ->label('audits.resource', 'user/{response.userId}') + ->label('audits.userId', '{response.userId}') ->label('sdk.auth', []) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updateMagicURLSession') @@ -763,9 +763,8 @@ App::put('/v1/account/sessions/magic-url') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->inject('audits') ->inject('events') - ->action(function (string $userId, string $secret, Request $request, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audits, Event $events) { + ->action(function (string $userId, string $secret, Request $request, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Event $events) { /** @var Utopia\Database\Document $user */ @@ -831,8 +830,6 @@ App::put('/v1/account/sessions/magic-url') throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB'); } - $audits->setResource('user/' . $user->getId()); - $events ->setParam('userId', $user->getId()) ->setParam('sessionId', $session->getId()) @@ -865,6 +862,8 @@ App::post('/v1/account/sessions/phone') ->groups(['api', 'account']) ->label('scope', 'public') ->label('auth.type', 'phone') + ->label('audits.resource', 'user/{response.userId}') + ->label('audits.userId', '{response.userId}') ->label('sdk.auth', []) ->label('sdk.namespace', 'account') ->label('sdk.method', 'createPhoneSession') @@ -875,17 +874,16 @@ App::post('/v1/account/sessions/phone') ->label('abuse-limit', 10) ->label('abuse-key', 'url:{url},email:{param-email}') ->param('userId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') - ->param('number', '', new ValidatorPhone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') + ->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') ->inject('request') ->inject('response') ->inject('project') ->inject('dbForProject') - ->inject('audits') ->inject('events') ->inject('messaging') - ->inject('phone') - ->action(function (string $userId, string $number, Request $request, Response $response, Document $project, Database $dbForProject, Audit $audits, Event $events, EventPhone $messaging, Phone $phone) { - if (empty(App::getEnv('_APP_PHONE_PROVIDER'))) { + ->action(function (string $userId, string $phone, Request $request, Response $response, Document $project, Database $dbForProject, Event $events, EventPhone $messaging) { + + if (empty(App::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -893,7 +891,7 @@ App::post('/v1/account/sessions/phone') $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); - $user = $dbForProject->findOne('users', [Query::equal('phone', [$number])]); + $user = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]); if (!$user) { $limit = $project->getAttribute('auths', [])['limit'] ?? 0; @@ -916,7 +914,7 @@ App::post('/v1/account/sessions/phone') Permission::delete(Role::user($userId)), ], 'email' => null, - 'phone' => $number, + 'phone' => $phone, 'emailVerification' => false, 'phoneVerification' => false, 'status' => true, @@ -928,11 +926,11 @@ App::post('/v1/account/sessions/phone') 'sessions' => null, 'tokens' => null, 'memberships' => null, - 'search' => implode(' ', [$userId, $number]) + 'search' => implode(' ', [$userId, $phone]) ]))); } - $secret = $phone->generateSecretDigits(); + $secret = (App::getEnv('_APP_SMS_PROVIDER') === 'sms://mock') ? Mock::$digits : Auth::codeGenerator(); $expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_PHONE); $token = new Document([ @@ -958,7 +956,7 @@ App::post('/v1/account/sessions/phone') $dbForProject->deleteCachedDocument('users', $user->getId()); $messaging - ->setRecipient($number) + ->setRecipient($phone) ->setMessage($secret) ->trigger(); @@ -972,11 +970,6 @@ App::post('/v1/account/sessions/phone') // Hide secret for clients $token->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $secret : ''); - $audits - ->setResource('user/' . $user->getId()) - ->setUser($user) - ; - $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($token, Response::MODEL_TOKEN) @@ -984,7 +977,7 @@ App::post('/v1/account/sessions/phone') }); App::put('/v1/account/sessions/phone') - ->desc('Create Phone session (confirmation)') + ->desc('Create Phone Session (confirmation)') ->groups(['api', 'account']) ->label('scope', 'public') ->label('event', 'users.[userId].sessions.[sessionId].create') @@ -1004,9 +997,8 @@ App::put('/v1/account/sessions/phone') ->inject('dbForProject') ->inject('locale') ->inject('geodb') - ->inject('audits') ->inject('events') - ->action(function (string $userId, string $secret, Request $request, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audits, Event $events) { + ->action(function (string $userId, string $secret, Request $request, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Event $events) { $user = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId)); @@ -1068,8 +1060,6 @@ App::put('/v1/account/sessions/phone') throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB'); } - $audits->setResource('user/' . $user->getId()); - $events ->setParam('userId', $user->getId()) ->setParam('sessionId', $session->getId()) @@ -1103,6 +1093,8 @@ App::post('/v1/account/sessions/anonymous') ->label('event', 'users.[userId].sessions.[sessionId].create') ->label('scope', 'public') ->label('auth.type', 'anonymous') + ->label('audits.resource', 'user/{response.userId}') + ->label('audits.userId', '{response.userId}') ->label('sdk.auth', []) ->label('sdk.namespace', 'account') ->label('sdk.method', 'createAnonymousSession') @@ -1119,10 +1111,9 @@ App::post('/v1/account/sessions/anonymous') ->inject('project') ->inject('dbForProject') ->inject('geodb') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Audit $audits, Stats $usage, Event $events) { + ->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Stats $usage, Event $events) { $protocol = $request->getProtocol(); @@ -1156,6 +1147,8 @@ App::post('/v1/account/sessions/anonymous') 'emailVerification' => false, 'status' => true, 'password' => null, + 'hash' => Auth::DEFAULT_ALGO, + 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, 'passwordUpdate' => null, 'registration' => DateTime::now(), 'reset' => false, @@ -1201,8 +1194,6 @@ App::post('/v1/account/sessions/anonymous') $dbForProject->deleteCachedDocument('users', $user->getId()); - $audits->setResource('user/' . $user->getId()); - $usage ->setParam('users.sessions.create', 1) ->setParam('provider', 'anonymous') @@ -1289,15 +1280,13 @@ App::get('/v1/account') ->label('sdk.description', '/docs/references/account/get.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_USER) + ->label('sdk.response.model', Response::MODEL_ACCOUNT) ->inject('response') ->inject('user') ->inject('usage') ->action(function (Response $response, Document $user, Stats $usage) { - $usage->setParam('users.read', 1); - - $response->dynamic($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_ACCOUNT); }); App::get('/v1/account/prefs') @@ -1466,35 +1455,30 @@ App::patch('/v1/account/name') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.name') ->label('scope', 'account') + ->label('audits.resource', 'user/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updateName') ->label('sdk.description', '/docs/references/account/update-name.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_USER) + ->label('sdk.response.model', Response::MODEL_ACCOUNT) ->param('name', '', new Text(128), 'User name. Max length: 128 chars.') ->inject('response') ->inject('user') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $name, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { + ->action(function (string $name, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) { $user = $dbForProject->updateDocument('users', $user->getId(), $user ->setAttribute('name', $name) ->setAttribute('search', implode(' ', [$user->getId(), $name, $user->getAttribute('email', ''), $user->getAttribute('phone', '')]))); - $audits - ->setResource('user/' . $user->getId()) - ->setUser($user) - ; - $usage->setParam('users.update', 1); $events->setParam('userId', $user->getId()); - $response->dynamic($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_ACCOUNT); }); App::patch('/v1/account/password') @@ -1502,35 +1486,34 @@ App::patch('/v1/account/password') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.password') ->label('scope', 'account') + ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updatePassword') ->label('sdk.description', '/docs/references/account/update-password.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_USER) + ->label('sdk.response.model', Response::MODEL_ACCOUNT) ->param('password', '', new Password(), 'New user password. Must be at least 8 chars.') ->param('oldPassword', '', new Password(), 'Current user password. Must be at least 8 chars.', true) ->inject('response') ->inject('user') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $password, string $oldPassword, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { + ->action(function (string $password, string $oldPassword, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) { // Check old password only if its an existing user. - if ($user->getAttribute('passwordUpdate') !== null && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'))) { // Double check user password + if ($user->getAttribute('passwordUpdate') !== null && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password throw new Exception(Exception::USER_INVALID_CREDENTIALS); } - $user = $dbForProject->updateDocument( - 'users', - $user->getId(), - $user - ->setAttribute('password', Auth::passwordHash($password)) - ->setAttribute('passwordUpdate', DateTime::now()) - ); + $user = $dbForProject->updateDocument('users', $user->getId(), $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('passwordUpdate', DateTime::now())); $audits ->setResource('user/' . $user->getId()) @@ -1540,7 +1523,7 @@ App::patch('/v1/account/password') $usage->setParam('users.update', 1); $events->setParam('userId', $user->getId()); - $response->dynamic($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_ACCOUNT); }); App::patch('/v1/account/email') @@ -1548,28 +1531,27 @@ App::patch('/v1/account/email') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.email') ->label('scope', 'account') + ->label('audits.resource', 'user/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updateEmail') ->label('sdk.description', '/docs/references/account/update-email.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_USER) + ->label('sdk.response.model', Response::MODEL_ACCOUNT) ->param('email', '', new Email(), 'User email.') ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') ->inject('response') ->inject('user') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $email, string $password, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { - + ->action(function (string $email, string $password, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) { $isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting if ( !$isAnonymousUser && - !Auth::passwordVerify($password, $user->getAttribute('password')) + !Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions')) ) { // Double check user password throw new Exception(Exception::USER_INVALID_CREDENTIALS); } @@ -1577,7 +1559,9 @@ App::patch('/v1/account/email') $email = \strtolower($email); $user - ->setAttribute('password', $isAnonymousUser ? Auth::passwordHash($password) : $user->getAttribute('password', '')) + ->setAttribute('password', $isAnonymousUser ? Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS) : $user->getAttribute('password', '')) + ->setAttribute('hash', $isAnonymousUser ? Auth::DEFAULT_ALGO : $user->getAttribute('hash', '')) + ->setAttribute('hashOptions', $isAnonymousUser ? Auth::DEFAULT_ALGO_OPTIONS : $user->getAttribute('hashOptions', '')) ->setAttribute('email', $email) ->setAttribute('emailVerification', false) // After this user needs to confirm mail again ->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $email, $user->getAttribute('phone', '')])); @@ -1588,15 +1572,10 @@ App::patch('/v1/account/email') throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } - $audits - ->setResource('user/' . $user->getId()) - ->setUser($user) - ; - $usage->setParam('users.update', 1); $events->setParam('userId', $user->getId()); - $response->dynamic($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_ACCOUNT); }); App::patch('/v1/account/phone') @@ -1604,28 +1583,28 @@ App::patch('/v1/account/phone') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.phone') ->label('scope', 'account') + ->label('audits.resource', 'user/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updatePhone') ->label('sdk.description', '/docs/references/account/update-phone.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_USER) - ->param('number', '', new ValidatorPhone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') + ->label('sdk.response.model', Response::MODEL_ACCOUNT) + ->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('response') ->inject('user') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $phone, string $password, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { + ->action(function (string $phone, string $password, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) { $isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting if ( !$isAnonymousUser && - !Auth::passwordVerify($password, $user->getAttribute('password')) + !Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions')) ) { // Double check user password throw new Exception(Exception::USER_INVALID_CREDENTIALS); } @@ -1641,15 +1620,10 @@ App::patch('/v1/account/phone') throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS); } - $audits - ->setResource('user/' . $user->getId()) - ->setUser($user) - ; - $usage->setParam('users.update', 1); $events->setParam('userId', $user->getId()); - $response->dynamic($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_ACCOUNT); }); App::patch('/v1/account/prefs') @@ -1657,29 +1631,28 @@ App::patch('/v1/account/prefs') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.prefs') ->label('scope', 'account') + ->label('audits.resource', 'user/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updatePrefs') ->label('sdk.description', '/docs/references/account/update-prefs.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_USER) + ->label('sdk.response.model', Response::MODEL_ACCOUNT) ->param('prefs', [], new Assoc(), 'Prefs key-value JSON object.') ->inject('response') ->inject('user') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (array $prefs, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { + ->action(function (array $prefs, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) { $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs)); - $audits->setResource('user/' . $user->getId()); $usage->setParam('users.update', 1); $events->setParam('userId', $user->getId()); - $response->dynamic($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_ACCOUNT); }); App::patch('/v1/account/status') @@ -1687,31 +1660,27 @@ App::patch('/v1/account/status') ->groups(['api', 'account']) ->label('event', 'users.[userId].update.status') ->label('scope', 'account') + ->label('audits.resource', 'user/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updateStatus') ->label('sdk.description', '/docs/references/account/update-status.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_USER) + ->label('sdk.response.model', Response::MODEL_ACCOUNT) ->inject('request') ->inject('response') ->inject('user') ->inject('dbForProject') - ->inject('audits') ->inject('events') ->inject('usage') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Audit $audits, Event $events, Stats $usage) { + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events, Stats $usage) { $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', false)); - $audits - ->setResource('user/' . $user->getId()) - ->setPayload($response->output($user, Response::MODEL_USER)); - $events ->setParam('userId', $user->getId()) - ->setPayload($response->output($user, Response::MODEL_USER)); + ->setPayload($response->output($user, Response::MODEL_ACCOUNT)); if (!Config::getParam('domainVerification')) { $response->addHeader('X-Fallback-Cookies', \json_encode([])); @@ -1719,7 +1688,7 @@ App::patch('/v1/account/status') $usage->setParam('users.delete', 1); - $response->dynamic($user, Response::MODEL_USER); + $response->dynamic($user, Response::MODEL_ACCOUNT); }); App::delete('/v1/account/sessions/:sessionId') @@ -1727,6 +1696,7 @@ App::delete('/v1/account/sessions/:sessionId') ->groups(['api', 'account']) ->label('scope', 'account') ->label('event', 'users.[userId].sessions.[sessionId].delete') + ->label('audits.resource', 'user/{user.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'deleteSession') @@ -1740,10 +1710,9 @@ App::delete('/v1/account/sessions/:sessionId') ->inject('user') ->inject('dbForProject') ->inject('locale') - ->inject('audits') ->inject('events') ->inject('usage') - ->action(function (?string $sessionId, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Audit $audits, Event $events, Stats $usage) { + ->action(function (?string $sessionId, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $events, Stats $usage) { $protocol = $request->getProtocol(); $sessionId = ($sessionId === 'current') @@ -1758,8 +1727,6 @@ App::delete('/v1/account/sessions/:sessionId') $dbForProject->deleteDocument('sessions', $session->getId()); - $audits->setResource('user/' . $user->getId()); - $session->setAttribute('current', false); if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too @@ -1804,6 +1771,8 @@ App::patch('/v1/account/sessions/:sessionId') ->groups(['api', 'account']) ->label('scope', 'account') ->label('event', 'users.[userId].sessions.[sessionId].update') + ->label('audits.resource', 'user/{response.userId}') + ->label('audits.userId', '{response.userId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updateSession') @@ -1819,10 +1788,9 @@ App::patch('/v1/account/sessions/:sessionId') ->inject('dbForProject') ->inject('project') ->inject('locale') - ->inject('audits') ->inject('events') ->inject('usage') - ->action(function (?string $sessionId, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Audit $audits, Event $events, Stats $usage) { + ->action(function (?string $sessionId, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Event $events, Stats $usage) { $sessionId = ($sessionId === 'current') ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) @@ -1866,8 +1834,6 @@ App::patch('/v1/account/sessions/:sessionId') $dbForProject->deleteCachedDocument('users', $user->getId()); - $audits->setResource('user/' . $user->getId()); - $events ->setParam('userId', $user->getId()) ->setParam('sessionId', $session->getId()) @@ -1891,6 +1857,7 @@ App::delete('/v1/account/sessions') ->groups(['api', 'account']) ->label('scope', 'account') ->label('event', 'users.[userId].sessions.[sessionId].delete') + ->label('audits.resource', 'user/{user.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'deleteSessions') @@ -1903,10 +1870,9 @@ App::delete('/v1/account/sessions') ->inject('user') ->inject('dbForProject') ->inject('locale') - ->inject('audits') ->inject('events') ->inject('usage') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Audit $audits, Event $events, Stats $usage) { + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $events, Stats $usage) { $protocol = $request->getProtocol(); $sessions = $user->getAttribute('sessions', []); @@ -1914,8 +1880,6 @@ App::delete('/v1/account/sessions') foreach ($sessions as $session) {/** @var Document $session */ $dbForProject->deleteDocument('sessions', $session->getId()); - $audits->setResource('user/' . $user->getId()); - if (!Config::getParam('domainVerification')) { $response->addHeader('X-Fallback-Cookies', \json_encode([])); } @@ -1959,6 +1923,8 @@ App::post('/v1/account/recovery') ->groups(['api', 'account']) ->label('scope', 'public') ->label('event', 'users.[userId].recovery.[tokenId].create') + ->label('audits.resource', 'user/{response.userId}') + ->label('audits.userId', '{response.userId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'createRecovery') @@ -1976,10 +1942,9 @@ App::post('/v1/account/recovery') ->inject('project') ->inject('locale') ->inject('mails') - ->inject('audits') ->inject('events') ->inject('usage') - ->action(function (string $email, string $url, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Mail $mails, Audit $audits, Event $events, Stats $usage) { + ->action(function (string $email, string $url, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Mail $mails, Event $events, Stats $usage) { if (empty(App::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); @@ -2054,7 +2019,6 @@ App::post('/v1/account/recovery') // Hide secret for clients $recovery->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $secret : ''); - $audits->setResource('user/' . $profile->getId()); $usage->setParam('users.update', 1); $response->setStatusCode(Response::STATUS_CODE_CREATED); @@ -2066,6 +2030,8 @@ App::put('/v1/account/recovery') ->groups(['api', 'account']) ->label('scope', 'public') ->label('event', 'users.[userId].recovery.[tokenId].update') + ->label('audits.resource', 'user/{response.userId}') + ->label('audits.userId', '{response.userId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updateRecovery') @@ -2081,11 +2047,9 @@ App::put('/v1/account/recovery') ->param('passwordAgain', '', new Password(), 'Repeat new user password. Must be at least 8 chars.') ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { - + ->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Database $dbForProject, Stats $usage, Event $events) { if ($password !== $passwordAgain) { throw new Exception(Exception::USER_PASSWORD_MISMATCH); } @@ -2106,7 +2070,9 @@ App::put('/v1/account/recovery') Authorization::setRole(Role::user($profile->getId())->toString()); $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile - ->setAttribute('password', Auth::passwordHash($password)) + ->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS)) + ->setAttribute('hash', Auth::DEFAULT_ALGO) + ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS) ->setAttribute('passwordUpdate', DateTime::now()) ->setAttribute('emailVerification', true)); @@ -2119,8 +2085,6 @@ App::put('/v1/account/recovery') $dbForProject->deleteDocument('tokens', $recovery); $dbForProject->deleteCachedDocument('users', $profile->getId()); - $audits->setResource('user/' . $profile->getId()); - $usage->setParam('users.update', 1); $events @@ -2136,6 +2100,7 @@ App::post('/v1/account/verification') ->groups(['api', 'account']) ->label('scope', 'account') ->label('event', 'users.[userId].verification.[tokenId].create') + ->label('audits.resource', 'user/{response.userId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'createVerification') @@ -2152,11 +2117,10 @@ App::post('/v1/account/verification') ->inject('user') ->inject('dbForProject') ->inject('locale') - ->inject('audits') ->inject('events') ->inject('mails') ->inject('usage') - ->action(function (string $url, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Audit $audits, Event $events, Mail $mails, Stats $usage) { + ->action(function (string $url, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Event $events, Mail $mails, Stats $usage) { if (empty(App::getEnv('_APP_SMTP_HOST'))) { throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled'); @@ -2215,7 +2179,6 @@ App::post('/v1/account/verification') // Hide secret for clients $verification->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $verificationSecret : ''); - $audits->setResource('user/' . $user->getId()); $usage->setParam('users.update', 1); $response->setStatusCode(Response::STATUS_CODE_CREATED); @@ -2227,6 +2190,7 @@ App::put('/v1/account/verification') ->groups(['api', 'account']) ->label('scope', 'public') ->label('event', 'users.[userId].verification.[tokenId].update') + ->label('audits.resource', 'user/{response.userId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updateVerification') @@ -2241,10 +2205,9 @@ App::put('/v1/account/verification') ->inject('response') ->inject('user') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { + ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) { $profile = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId)); @@ -2272,8 +2235,6 @@ App::put('/v1/account/verification') $dbForProject->deleteDocument('tokens', $verification); $dbForProject->deleteCachedDocument('users', $profile->getId()); - $audits->setResource('user/' . $user->getId()); - $usage->setParam('users.update', 1); $events @@ -2289,6 +2250,7 @@ App::post('/v1/account/verification/phone') ->groups(['api', 'account']) ->label('scope', 'account') ->label('event', 'users.[userId].verification.[tokenId].create') + ->label('audits.resource', 'user/{response.userId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'createPhoneVerification') @@ -2300,17 +2262,15 @@ App::post('/v1/account/verification/phone') ->label('abuse-key', 'userId:{userId}') ->inject('request') ->inject('response') - ->inject('phone') ->inject('user') ->inject('dbForProject') - ->inject('audits') ->inject('events') ->inject('usage') ->inject('messaging') - ->action(function (Request $request, Response $response, Phone $phone, Document $user, Database $dbForProject, Audit $audits, Event $events, Stats $usage, EventPhone $messaging) { + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events, Stats $usage, EventPhone $messaging) { - if (empty(App::getEnv('_APP_PHONE_PROVIDER'))) { - throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); + if (empty(App::getEnv('_APP_SMS_PROVIDER'))) { + throw new Exception(Exception::GENERAL_PHONE_DISABLED); } if (empty($user->getAttribute('phone'))) { @@ -2321,7 +2281,7 @@ App::post('/v1/account/verification/phone') $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); $verificationSecret = Auth::tokenGenerator(); - $secret = $phone->generateSecretDigits(); + $secret = (App::getEnv('_APP_SMS_PROVIDER') === 'sms://mock') ? Mock::$digits : Auth::codeGenerator(); $expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM); $verification = new Document([ @@ -2364,7 +2324,6 @@ App::post('/v1/account/verification/phone') // Hide secret for clients $verification->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $verificationSecret : ''); - $audits->setResource('user/' . $user->getId()); $usage->setParam('users.update', 1); $response->setStatusCode(Response::STATUS_CODE_CREATED); @@ -2376,6 +2335,7 @@ App::put('/v1/account/verification/phone') ->groups(['api', 'account']) ->label('scope', 'public') ->label('event', 'users.[userId].verification.[tokenId].update') + ->label('audits.resource', 'user/{response.userId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updatePhoneVerification') @@ -2390,10 +2350,9 @@ App::put('/v1/account/verification/phone') ->inject('response') ->inject('user') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { + ->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) { $profile = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId)); @@ -2419,8 +2378,6 @@ App::put('/v1/account/verification/phone') $dbForProject->deleteDocument('tokens', $verification); $dbForProject->deleteCachedDocument('users', $profile->getId()); - $audits->setResource('user/' . $user->getId()); - $usage->setParam('users.update', 1); $events diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index afdd7d4daf..6dc31787b5 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -7,8 +7,6 @@ use Appwrite\Utopia\Response; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; use Utopia\App; -use Utopia\Cache\Adapter\Filesystem; -use Utopia\Cache\Cache; use Utopia\Config\Config; use Utopia\Database\Document; use Utopia\Image\Image; @@ -37,8 +35,6 @@ $avatarCallback = function (string $type, string $code, int $width, int $height, } $output = 'png'; - $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache - $key = \md5('/v1/avatars/' . $type . '/:code-' . $code . $width . $height . $quality . $output); $path = $set[$code]; $type = 'png'; @@ -46,35 +42,15 @@ $avatarCallback = function (string $type, string $code, int $width, int $height, throw new Exception(Exception::GENERAL_SERVER_ERROR, 'File not readable in ' . $path); } - $cache = new Cache(new Filesystem(APP_STORAGE_CACHE . '/app-0')); // Limit file number or size - $data = $cache->load($key, 60 * 60 * 24 * 30 * 3/* 3 months */); - - if ($data) { - //$output = (empty($output)) ? $type : $output; - - return $response - ->setContentType('image/png') - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'hit') - ->send($data); - } - $image = new Image(\file_get_contents($path)); - $image->crop((int) $width, (int) $height); - $output = (empty($output)) ? $type : $output; - $data = $image->output($output, $quality); - - $cache->save($key, $data); - $response + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT') ->setContentType('image/png') - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'miss') - ->send($data, null); - + ->file($data) + ; unset($image); }; @@ -82,6 +58,8 @@ App::get('/v1/avatars/credit-cards/:code') ->desc('Get Credit Card Icon') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resource', 'avatar/credit-card') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getCreditCard') @@ -100,6 +78,8 @@ App::get('/v1/avatars/browsers/:code') ->desc('Get Browser Icon') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resource', 'avatar/browser') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getBrowser') @@ -118,6 +98,8 @@ App::get('/v1/avatars/flags/:code') ->desc('Get Country Flag') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resource', 'avatar/flag') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getFlag') @@ -136,6 +118,8 @@ App::get('/v1/avatars/image') ->desc('Get Image from URL') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resource', 'avatar/image') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getImage') @@ -151,19 +135,7 @@ App::get('/v1/avatars/image') $quality = 80; $output = 'png'; - $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache - $key = \md5('/v2/avatars/images-' . $url . '-' . $width . '/' . $height . '/' . $quality); $type = 'png'; - $cache = new Cache(new Filesystem(APP_STORAGE_CACHE . '/app-0')); // Limit file number or size - $data = $cache->load($key, 60 * 60 * 24 * 7/* 1 week */); - - if ($data) { - return $response - ->setContentType('image/png') - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'hit') - ->send($data); - } if (!\extension_loaded('imagick')) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); @@ -182,19 +154,14 @@ App::get('/v1/avatars/image') } $image->crop((int) $width, (int) $height); - $output = (empty($output)) ? $type : $output; - $data = $image->output($output, $quality); - $cache->save($key, $data); - $response + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT') ->setContentType('image/png') - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'miss') - ->send($data); - + ->file($data) + ; unset($image); }); @@ -202,6 +169,8 @@ App::get('/v1/avatars/favicon') ->desc('Get Favicon') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resource', 'avatar/favicon') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getFavicon') @@ -217,19 +186,7 @@ App::get('/v1/avatars/favicon') $height = 56; $quality = 80; $output = 'png'; - $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache - $key = \md5('/v2/avatars/favicon-' . $url); $type = 'png'; - $cache = new Cache(new Filesystem(APP_STORAGE_CACHE . '/app-0')); // Limit file number or size - $data = $cache->load($key, 60 * 60 * 24 * 30 * 3/* 3 months */); - - if ($data) { - return $response - ->setContentType('image/png') - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'hit') - ->send($data); - } if (!\extension_loaded('imagick')) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); @@ -314,14 +271,11 @@ App::get('/v1/avatars/favicon') if (empty($data) || (\mb_substr($data, 0, 5) === 'save($key, $data); - - return $response + $response + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT') ->setContentType('image/x-icon') - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'miss') - ->send($data); + ->file($data) + ; } $fetch = @\file_get_contents($outputHref, false); @@ -331,21 +285,15 @@ App::get('/v1/avatars/favicon') } $image = new Image($fetch); - $image->crop((int) $width, (int) $height); - $output = (empty($output)) ? $type : $output; - $data = $image->output($output, $quality); - $cache->save($key, $data); - $response + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT') ->setContentType('image/png') - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'miss') - ->send($data); - + ->file($data) + ; unset($image); }); @@ -381,19 +329,21 @@ App::get('/v1/avatars/qr') } $image = new Image($qrcode->render($text)); - $image->crop((int) $size, (int) $size); $response ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache ->setContentType('image/png') - ->send($image->output('png', 9)); + ->send($image->output('png', 9)) + ; }); App::get('/v1/avatars/initials') ->desc('Get User Initials') ->groups(['api', 'avatars']) ->label('scope', 'avatars.read') + ->label('cache', true) + ->label('cache.resource', 'avatar/initials') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'avatars') ->label('sdk.method', 'getInitials') @@ -468,5 +418,6 @@ App::get('/v1/avatars/initials') $response ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache ->setContentType('image/png') - ->send($image->getImageBlob()); + ->file($image->getImageBlob()) + ; }); diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 4cb17c2d55..eebee987e1 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -42,7 +42,6 @@ use Appwrite\Utopia\Database\Validator\Queries as QueriesValidator; use Appwrite\Utopia\Database\Validator\OrderAttributes; use Appwrite\Utopia\Response; use Appwrite\Detector\Detector; -use Appwrite\Event\Audit as EventAudit; use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; use Appwrite\Stats\Stats; @@ -56,7 +55,7 @@ use MaxMind\Db\Reader; * @return Document Newly created attribute document * @throws Exception */ -function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Event $events, Stats $usage): Document +function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $database, Event $events, Stats $usage): Document { $key = $attribute->getAttribute('key'); $type = $attribute->getAttribute('type', ''); @@ -147,11 +146,6 @@ function createAttribute(string $databaseId, string $collectionId, Document $att ->setParam('attributeId', $attribute->getId()) ; - $audits - ->setResource('database/' . $db->getId() . '/collection/' . $collectionId) - ->setPayload($attribute->getArrayCopy()) - ; - $response->setStatusCode(Response::STATUS_CODE_CREATED); return $attribute; @@ -162,6 +156,7 @@ App::post('/v1/databases') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].create') ->label('scope', 'databases.write') + ->label('audits.resource', 'database/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'create') @@ -173,10 +168,9 @@ App::post('/v1/databases') ->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.') ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $name, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) { $databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId; @@ -224,11 +218,6 @@ App::post('/v1/databases') throw new Exception(Exception::DATABASE_ALREADY_EXISTS); } - $audits - ->setResource('database/' . $databaseId) - ->setPayload($database->getArrayCopy()) - ; - $events->setParam('databaseId', $database->getId()); $usage->setParam('databases.create', 1); @@ -398,6 +387,7 @@ App::put('/v1/databases/:databaseId') ->groups(['api', 'database']) ->label('scope', 'databases.write') ->label('event', 'databases.[databaseId].update') + ->label('audits.resource', 'database/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'update') @@ -409,10 +399,9 @@ App::put('/v1/databases/:databaseId') ->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.') ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $name, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) { $database = $dbForProject->getDocument('databases', $databaseId); @@ -430,11 +419,6 @@ App::put('/v1/databases/:databaseId') throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, 'Bad structure. ' . $exception->getMessage()); } - $audits - ->setResource('database/' . $databaseId) - ->setPayload($database->getArrayCopy()) - ; - $usage->setParam('databases.update', 1); $events->setParam('databaseId', $database->getId()); @@ -446,6 +430,7 @@ App::delete('/v1/databases/:databaseId') ->groups(['api', 'database']) ->label('scope', 'databases.write') ->label('event', 'databases.[databaseId].delete') + ->label('audits.resource', 'database/{request.databaseId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'delete') @@ -456,10 +441,9 @@ App::delete('/v1/databases/:databaseId') ->inject('response') ->inject('dbForProject') ->inject('events') - ->inject('audits') ->inject('deletes') ->inject('usage') - ->action(function (string $databaseId, Response $response, Database $dbForProject, Event $events, EventAudit $audits, Delete $deletes, Stats $usage) { + ->action(function (string $databaseId, Response $response, Database $dbForProject, Event $events, Delete $deletes, Stats $usage) { $database = $dbForProject->getDocument('databases', $databaseId); @@ -483,11 +467,6 @@ App::delete('/v1/databases/:databaseId') ->setPayload($response->output($database, Response::MODEL_DATABASE)) ; - $audits - ->setResource('database/' . $databaseId) - ->setPayload($database->getArrayCopy()) - ; - $usage->setParam('databases.delete', 1); $response->noContent(); @@ -499,6 +478,7 @@ App::post('/v1/databases/:databaseId/collections') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].create') ->label('scope', 'collections.write') + ->label('audits.resource', 'database/{request.databaseId}/collection/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'createCollection') @@ -513,10 +493,9 @@ App::post('/v1/databases/:databaseId/collections') ->param('documentSecurity', false, new Boolean(true), 'Specifies the permissions model used in this collection, which accepts either \'collection\' or \'document\'. For \'collection\' level permission, the permissions specified in read and write params are applied to all documents in the collection. For \'document\' level permissions, read and write permissions are specified in each document. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.') ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, Response $response, Database $dbForProject, Stats $usage, Event $events) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -535,9 +514,9 @@ App::post('/v1/databases/:databaseId/collections') try { $dbForProject->createDocument('database_' . $database->getInternalId(), new Document([ '$id' => $collectionId, - '$permissions' => $permissions ?? [], 'databaseInternalId' => $database->getInternalId(), 'databaseId' => $databaseId, + '$permissions' => $permissions ?? [], 'documentSecurity' => $documentSecurity, 'enabled' => true, 'name' => $name, @@ -552,11 +531,6 @@ App::post('/v1/databases/:databaseId/collections') throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED); } - $audits - ->setResource('database/' . $databaseId . '/collection/' . $collectionId) - ->setPayload($collection->getArrayCopy()) - ; - $events ->setContext('database', $database) ->setParam('databaseId', $databaseId) @@ -766,6 +740,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') ->groups(['api', 'database']) ->label('scope', 'collections.write') ->label('event', 'databases.[databaseId].collections.[collectionId].update') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'updateCollection') @@ -781,10 +756,9 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') ->param('enabled', true, new Boolean(), 'Is collection enabled?', true) ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, Stats $usage, Event $events) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -820,11 +794,6 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, 'Bad structure. ' . $exception->getMessage()); } - $audits - ->setResource('database/' . $databaseId . '/collection/' . $collectionId) - ->setPayload($collection->getArrayCopy()) - ; - $usage ->setParam('databaseId', $databaseId) ->setParam('databases.collections.update', 1); @@ -843,6 +812,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId') ->groups(['api', 'database']) ->label('scope', 'collections.write') ->label('event', 'databases.[databaseId].collections.[collectionId].delete') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'deleteCollection') @@ -854,10 +824,9 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId') ->inject('response') ->inject('dbForProject') ->inject('events') - ->inject('audits') ->inject('deletes') ->inject('usage') - ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, Event $events, EventAudit $audits, Delete $deletes, Stats $usage) { + ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, Event $events, Delete $deletes, Stats $usage) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -889,11 +858,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId') ->setPayload($response->output($collection, Response::MODEL_COLLECTION)) ; - $audits - ->setResource('database/' . $databaseId . '/collection/' . $collectionId) - ->setPayload($collection->getArrayCopy()) - ; - $usage ->setParam('databaseId', $databaseId) ->setParam('databases.collections.delete', 1); @@ -907,6 +871,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'createStringAttribute') @@ -915,7 +880,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_STRING) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Range::TYPE_INTEGER), 'Attribute size for text attributes, in number of characters.') ->param('required', null, new Boolean(), 'Is attribute required?') @@ -924,10 +889,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string ->inject('response') ->inject('dbForProject') ->inject('database') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $collectionId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $collectionId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) { // Ensure attribute default is within required size $validator = new Text($size); @@ -942,7 +906,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string 'required' => $required, 'default' => $default, 'array' => $array, - ]), $response, $dbForProject, $database, $audits, $events, $usage); + ]), $response, $dbForProject, $database, $events, $usage); $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING); @@ -954,6 +918,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email' ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createEmailAttribute') @@ -962,7 +927,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email' ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_EMAIL) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('default', null, new Email(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true) @@ -970,10 +935,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email' ->inject('response') ->inject('dbForProject') ->inject('database') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -983,7 +947,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email' 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_EMAIL, - ]), $response, $dbForProject, $database, $audits, $events, $usage); + ]), $response, $dbForProject, $database, $events, $usage); $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL); @@ -995,6 +959,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createEnumAttribute') @@ -1003,7 +968,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_ENUM) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('elements', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of elements in enumerated type. Uses length of longest element to determine size. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' elements are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.') ->param('required', null, new Boolean(), 'Is attribute required?') @@ -1012,10 +977,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') ->inject('response') ->inject('dbForProject') ->inject('database') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) { // use length of longest string as attribute size $size = 0; @@ -1040,7 +1004,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum') 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_ENUM, 'formatOptions' => ['elements' => $elements], - ]), $response, $dbForProject, $database, $audits, $events, $usage); + ]), $response, $dbForProject, $database, $events, $usage); $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_ENUM); @@ -1052,6 +1016,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createIpAttribute') @@ -1060,7 +1025,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_IP) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('default', null, new IP(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true) @@ -1068,10 +1033,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->inject('response') ->inject('dbForProject') ->inject('database') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1081,7 +1045,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_IP, - ]), $response, $dbForProject, $database, $audits, $events, $usage); + ]), $response, $dbForProject, $database, $events, $usage); $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP); @@ -1093,6 +1057,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createUrlAttribute') @@ -1101,7 +1066,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_URL) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('default', null, new URL(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true) @@ -1109,10 +1074,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->inject('response') ->inject('dbForProject') ->inject('database') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1122,7 +1086,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') 'default' => $default, 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_URL, - ]), $response, $dbForProject, $database, $audits, $events, $usage); + ]), $response, $dbForProject, $database, $events, $usage); $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_URL); @@ -1134,6 +1098,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createIntegerAttribute') @@ -1142,7 +1107,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_INTEGER) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('min', null, new Integer(), 'Minimum value to enforce on new documents', true) @@ -1152,10 +1117,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege ->inject('response') ->inject('dbForProject') ->inject('database') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) { // Ensure attribute default is within range $min = (is_null($min)) ? PHP_INT_MIN : \intval($min); @@ -1185,7 +1149,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege 'min' => $min, 'max' => $max, ], - ]), $response, $dbForProject, $database, $audits, $events, $usage); + ]), $response, $dbForProject, $database, $events, $usage); $formatOptions = $attribute->getAttribute('formatOptions', []); @@ -1204,6 +1168,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createFloatAttribute') @@ -1212,7 +1177,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_FLOAT) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('min', null, new FloatValidator(), 'Minimum value to enforce on new documents', true) @@ -1222,10 +1187,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' ->inject('response') ->inject('dbForProject') ->inject('database') - ->inject('audits') ->inject('events') ->inject('usage') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Event $events, Stats $usage) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Event $events, Stats $usage) { // Ensure attribute default is within range $min = (is_null($min)) ? -PHP_FLOAT_MAX : \floatval($min); @@ -1258,7 +1222,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' 'min' => $min, 'max' => $max, ], - ]), $response, $dbForProject, $database, $audits, $events, $usage); + ]), $response, $dbForProject, $database, $events, $usage); $formatOptions = $attribute->getAttribute('formatOptions', []); @@ -1277,6 +1241,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create') ->label('scope', 'collections.write') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.namespace', 'databases') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createBooleanAttribute') @@ -1285,7 +1250,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_BOOLEAN) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('default', null, new Boolean(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true) @@ -1293,10 +1258,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea ->inject('response') ->inject('dbForProject') ->inject('database') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) { $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, @@ -1305,7 +1269,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea 'required' => $required, 'default' => $default, 'array' => $array, - ]), $response, $dbForProject, $database, $audits, $events, $usage); + ]), $response, $dbForProject, $database, $events, $usage); $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_BOOLEAN); @@ -1367,7 +1331,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_LIST) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->inject('response') ->inject('dbForProject') ->inject('usage') @@ -1419,7 +1383,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key') Response::MODEL_ATTRIBUTE_DATETIME, Response::MODEL_ATTRIBUTE_STRING])// needs to be last, since its condition would dominate any other string attribute ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->inject('response') ->inject('dbForProject') @@ -1476,6 +1440,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->groups(['api', 'database']) ->label('scope', 'collections.write') ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].delete') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'deleteAttribute') @@ -1483,15 +1448,14 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_NONE) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->inject('response') ->inject('dbForProject') ->inject('database') ->inject('events') - ->inject('audits') ->inject('usage') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $database, Event $events, EventAudit $audits, Stats $usage) { + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $database, Event $events, Stats $usage) { $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -1557,11 +1521,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->setPayload($response->output($attribute, $model)) ; - $audits - ->setResource('database/' . $databaseId . '/collection/' . $collectionId) - ->setPayload($attribute->getArrayCopy()) - ; - $response->noContent(); }); @@ -1571,6 +1530,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].create') ->label('scope', 'collections.write') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'createIndex') @@ -1579,7 +1539,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_INDEX) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('key', null, new Key(), 'Index Key.') ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL, Database::INDEX_ARRAY]), 'Index type.') ->param('attributes', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.') @@ -1587,10 +1547,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->inject('response') ->inject('dbForProject') ->inject('database') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) { $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -1712,11 +1671,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->setContext('database', $db) ; - $audits - ->setResource('database/' . $databaseId . '/collection/' . $collection->getId()) - ->setPayload($index->getArrayCopy()) - ; - $response->setStatusCode(Response::STATUS_CODE_ACCEPTED); $response->dynamic($index, Response::MODEL_INDEX); }); @@ -1734,7 +1688,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_INDEX_LIST) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->inject('response') ->inject('dbForProject') ->inject('usage') @@ -1776,7 +1730,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_INDEX) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('key', null, new Key(), 'Index Key.') ->inject('response') ->inject('dbForProject') @@ -1820,6 +1774,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->groups(['api', 'database']) ->label('scope', 'collections.write') ->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].delete') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'deleteIndex') @@ -1827,15 +1782,14 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_NONE) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('key', '', new Key(), 'Index Key.') ->inject('response') ->inject('dbForProject') ->inject('database') ->inject('events') - ->inject('audits') ->inject('usage') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $database, Event $events, EventAudit $audits, Stats $usage) { + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $database, Event $events, Stats $usage) { $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -1881,11 +1835,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->setPayload($response->output($index, Response::MODEL_INDEX)) ; - $audits - ->setResource('database/' . $databaseId . '/collection/' . $collection->getId()) - ->setPayload($index->getArrayCopy()) - ; - $response->noContent(); }); @@ -1895,6 +1844,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].create') ->label('scope', 'documents.write') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'createDocument') @@ -1904,17 +1854,16 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->label('sdk.response.model', Response::MODEL_DOCUMENT) ->param('databaseId', '', new UID(), 'Database ID.') ->param('documentId', '', new CustomId(), 'Document ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') - ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection). Make sure to define attributes before creating documents.') + ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection). Make sure to define attributes before creating documents.') ->param('data', [], new JSON(), 'Document data as JSON object.') ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE]), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->inject('response') ->inject('dbForProject') ->inject('user') - ->inject('audits') ->inject('usage') ->inject('events') ->inject('mode') - ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, EventAudit $audits, Stats $usage, Event $events, string $mode) { + ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Stats $usage, Event $events, string $mode) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -2027,11 +1976,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->setParam('collectionId', $collectionId) ; - $audits - ->setResource('database/' . $databaseId . '/collection/' . $collectionId . '/document/' . $document->getId()) - ->setPayload($document->getArrayCopy()) - ; - $response->setStatusCode(Response::STATUS_CODE_CREATED); $response->dynamic($document, Response::MODEL_DOCUMENT); }); @@ -2049,8 +1993,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DOCUMENT_LIST) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') - ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) + ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') + ->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/database#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) ->param('limit', 25, new Range(0, 100), 'Maximum number of documents to return in response. By default will return maximum 25 results. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' results allowed per request.', true) ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true) ->param('cursor', '', new UID(), 'ID of the document used as the starting point for the query, excluding the document itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true) @@ -2162,7 +2106,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DOCUMENT) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('documentId', null, new UID(), 'Document ID.') ->inject('response') ->inject('dbForProject') @@ -2318,6 +2262,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].update') ->label('scope', 'documents.write') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'updateDocument') @@ -2332,11 +2277,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events, string $mode) { + ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Stats $usage, Event $events, string $mode) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -2444,11 +2388,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->setParam('collectionId', $collectionId) ; - $audits - ->setResource('database/' . $databaseId . '/collection/' . $collectionId . '/document/' . $document->getId()) - ->setPayload($document->getArrayCopy()) - ; - $response->dynamic($document, Response::MODEL_DOCUMENT); }); @@ -2458,6 +2397,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->groups(['api', 'database']) ->label('scope', 'documents.write') ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].delete') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{request.documentId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'databases') ->label('sdk.method', 'deleteDocument') @@ -2465,16 +2405,15 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_NONE) ->param('databaseId', '', new UID(), 'Database ID.') - ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') + ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).') ->param('documentId', null, new UID(), 'Document ID.') ->inject('response') ->inject('dbForProject') ->inject('events') - ->inject('audits') ->inject('deletes') ->inject('usage') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, string $documentId, Response $response, Database $dbForProject, Event $events, EventAudit $audits, Delete $deletes, Stats $usage, string $mode) { + ->action(function (string $databaseId, string $collectionId, string $documentId, Response $response, Database $dbForProject, Event $events, Delete $deletes, Stats $usage, string $mode) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -2539,11 +2478,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->setPayload($response->output($document, Response::MODEL_DOCUMENT)) ; - $audits - ->setResource('database/' . $databaseId . '/collection/' . $collectionId . '/document/' . $document->getId()) - ->setPayload($document->getArrayCopy()) - ; - $response->noContent(); }); diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 4da9636f4d..2c38a529eb 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -47,6 +47,7 @@ App::post('/v1/functions') ->desc('Create Function') ->label('scope', 'functions.write') ->label('event', 'functions.[functionId].create') + ->label('audits.resource', 'function/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'create') @@ -293,6 +294,7 @@ App::put('/v1/functions/:functionId') ->desc('Update Function') ->label('scope', 'functions.write') ->label('event', 'functions.[functionId].update') + ->label('audits.resource', 'function/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'update') @@ -356,6 +358,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId') ->desc('Update Function Deployment') ->label('scope', 'functions.write') ->label('event', 'functions.[functionId].deployments.[deploymentId].update') + ->label('audits.resource', 'function/{request.functionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'updateDeployment') @@ -421,6 +424,7 @@ App::delete('/v1/functions/:functionId') ->desc('Delete Function') ->label('scope', 'functions.write') ->label('event', 'functions.[functionId].delete') + ->label('audits.resource', 'function/{request.functionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'delete') @@ -458,6 +462,7 @@ App::post('/v1/functions/:functionId/deployments') ->desc('Create Deployment') ->label('scope', 'functions.write') ->label('event', 'functions.[functionId].deployments.[deploymentId].create') + ->label('audits.resource', 'function/{request.functionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'createDeployment') @@ -751,6 +756,7 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId') ->desc('Delete Deployment') ->label('scope', 'functions.write') ->label('event', 'functions.[functionId].deployments.[deploymentId].delete') + ->label('audits.resource', 'function/{request.functionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'deleteDeployment') @@ -966,6 +972,7 @@ App::post('/v1/functions/:functionId/executions') $execution->setAttribute('status', $executionResponse['status']); $execution->setAttribute('statusCode', $executionResponse['statusCode']); $execution->setAttribute('response', $executionResponse['response']); + $execution->setAttribute('stdout', $executionResponse['stdout']); $execution->setAttribute('stderr', $executionResponse['stderr']); $execution->setAttribute('time', $executionResponse['time']); } catch (\Throwable $th) { @@ -986,6 +993,14 @@ App::post('/v1/functions/:functionId/executions') ->setParam('functionStatus', $execution->getAttribute('status', '')) ->setParam('functionExecutionTime', $execution->getAttribute('time') * 1000); // ms + $roles = Authorization::getRoles(); + $isPrivilegedUser = Auth::isPrivilegedUser($roles); + $isAppUser = Auth::isAppUser($roles); + if (!$isPrivilegedUser && !$isAppUser) { + $execution->setAttribute('stdout', ''); + $execution->setAttribute('stderr', ''); + } + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($execution, Response::MODEL_EXECUTION); @@ -1043,6 +1058,17 @@ App::get('/v1/functions/:functionId/executions') $results = $dbForProject->find('executions', \array_merge($filterQueries, $queries)); $total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT); + $roles = Authorization::getRoles(); + $isPrivilegedUser = Auth::isPrivilegedUser($roles); + $isAppUser = Auth::isAppUser($roles); + if (!$isPrivilegedUser && !$isAppUser) { + $results = array_map(function ($execution) { + $execution->setAttribute('stdout', ''); + $execution->setAttribute('stderr', ''); + return $execution; + }, $results); + } + $response->dynamic(new Document([ 'executions' => $results, 'total' => $total, @@ -1082,6 +1108,14 @@ App::get('/v1/functions/:functionId/executions/:executionId') throw new Exception(Exception::EXECUTION_NOT_FOUND); } + $roles = Authorization::getRoles(); + $isPrivilegedUser = Auth::isPrivilegedUser($roles); + $isAppUser = Auth::isAppUser($roles); + if (!$isPrivilegedUser && !$isAppUser) { + $execution->setAttribute('stdout', ''); + $execution->setAttribute('stderr', ''); + } + $response->dynamic($execution, Response::MODEL_EXECUTION); }); @@ -1090,6 +1124,7 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') ->desc('Retry Build') ->label('scope', 'functions.write') ->label('event', 'functions.[functionId].deployments.[deploymentId].update') + ->label('audits.resource', 'function/{request.functionId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'retryBuild') diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 65e536cf33..d8b755ba6b 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -136,6 +136,7 @@ App::post('/v1/projects') if (($collection['$collection'] ?? '') !== Database::METADATA) { continue; } + $attributes = []; $indexes = []; @@ -162,7 +163,6 @@ App::post('/v1/projects') 'orders' => $index['orders'], ]); } - $dbForProject->createCollection($key, $attributes, $indexes); } @@ -548,7 +548,7 @@ App::delete('/v1/projects/:projectId') ->inject('deletes') ->action(function (string $projectId, string $password, Response $response, Document $user, Database $dbForConsole, Delete $deletes) { - if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password + if (!Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password throw new Exception(Exception::USER_INVALID_CREDENTIALS); } diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 2dd53d2803..de69bd1351 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -2,7 +2,6 @@ use Appwrite\Auth\Auth; use Appwrite\ClamAV\Network; -use Appwrite\Event\Audit; use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Permissions\PermissionsProcessor; @@ -11,8 +10,6 @@ use Appwrite\OpenSSL\OpenSSL; use Appwrite\Stats\Stats; use Appwrite\Utopia\Response; use Utopia\App; -use Utopia\Cache\Adapter\Filesystem; -use Utopia\Cache\Cache; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Document; @@ -51,6 +48,7 @@ App::post('/v1/storage/buckets') ->groups(['api', 'storage']) ->label('scope', 'buckets.write') ->label('event', 'buckets.[bucketId].create') + ->label('audits.resource', 'buckets/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'storage') ->label('sdk.method', 'createBucket') @@ -69,10 +67,9 @@ App::post('/v1/storage/buckets') ->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true) ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { + ->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Stats $usage, Event $events) { $bucketId = $bucketId === 'unique()' ? ID::unique() : $bucketId; @@ -136,11 +133,6 @@ App::post('/v1/storage/buckets') throw new Exception(Exception::STORAGE_BUCKET_ALREADY_EXISTS); } - $audits - ->setResource('storage/buckets/' . $bucket->getId()) - ->setPayload($bucket->getArrayCopy()) - ; - $events ->setParam('bucketId', $bucket->getId()) ; @@ -234,6 +226,7 @@ App::put('/v1/storage/buckets/:bucketId') ->groups(['api', 'storage']) ->label('scope', 'buckets.write') ->label('event', 'buckets.[bucketId].update') + ->label('audits.resource', 'buckets/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'storage') ->label('sdk.method', 'updateBucket') @@ -252,10 +245,9 @@ App::put('/v1/storage/buckets/:bucketId') ->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true) ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { + ->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Stats $usage, Event $events) { $bucket = $dbForProject->getDocument('buckets', $bucketId); if ($bucket->isEmpty()) { @@ -285,11 +277,6 @@ App::put('/v1/storage/buckets/:bucketId') ->setAttribute('encryption', (bool) filter_var($encryption, FILTER_VALIDATE_BOOLEAN)) ->setAttribute('antivirus', (bool) filter_var($antivirus, FILTER_VALIDATE_BOOLEAN))); - $audits - ->setResource('storage/buckets/' . $bucket->getId()) - ->setPayload($bucket->getArrayCopy()) - ; - $events ->setParam('bucketId', $bucket->getId()) ; @@ -304,6 +291,7 @@ App::delete('/v1/storage/buckets/:bucketId') ->groups(['api', 'storage']) ->label('scope', 'buckets.write') ->label('event', 'buckets.[bucketId].delete') + ->label('audits.resource', 'buckets/{request.bucketId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'storage') ->label('sdk.method', 'deleteBucket') @@ -313,11 +301,10 @@ App::delete('/v1/storage/buckets/:bucketId') ->param('bucketId', '', new UID(), 'Bucket unique ID.') ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('deletes') ->inject('events') ->inject('usage') - ->action(function (string $bucketId, Response $response, Database $dbForProject, Audit $audits, Delete $deletes, Event $events, Stats $usage) { + ->action(function (string $bucketId, Response $response, Database $dbForProject, Delete $deletes, Event $events, Stats $usage) { $bucket = $dbForProject->getDocument('buckets', $bucketId); if ($bucket->isEmpty()) { @@ -337,11 +324,6 @@ App::delete('/v1/storage/buckets/:bucketId') ->setPayload($response->output($bucket, Response::MODEL_BUCKET)) ; - $audits - ->setResource('storage/buckets/' . $bucket->getId()) - ->setPayload($bucket->getArrayCopy()) - ; - $usage->setParam('storage.buckets.delete', 1); $response->noContent(); @@ -353,6 +335,7 @@ App::post('/v1/storage/buckets/:bucketId/files') ->groups(['api', 'storage']) ->label('scope', 'files.write') ->label('event', 'buckets.[bucketId].files.[fileId].create') + ->label('audits.resource', 'files/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'storage') ->label('sdk.method', 'createFile') @@ -370,13 +353,13 @@ App::post('/v1/storage/buckets/:bucketId/files') ->inject('response') ->inject('dbForProject') ->inject('user') - ->inject('audits') ->inject('usage') ->inject('events') ->inject('mode') ->inject('deviceFiles') ->inject('deviceLocal') - ->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Audit $audits, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal) { + ->inject('deletes') + ->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal, Delete $deletes) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); @@ -491,8 +474,8 @@ App::post('/v1/storage/buckets/:bucketId/files') } /** - * Validators - */ + * Validators + */ // Check if file type is allowed $allowedFileExtensions = $bucket->getAttribute('allowedFileExtensions', []); $fileExt = new FileExt($allowedFileExtensions); @@ -628,10 +611,6 @@ App::post('/v1/storage/buckets/:bucketId/files') throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); } - $audits - ->setResource('storage/files/' . $file->getId()) - ; - $usage ->setParam('storage', $sizeActual ?? 0) ->setParam('storage.files.create', 1) @@ -679,6 +658,11 @@ App::post('/v1/storage/buckets/:bucketId/files') ->setContext('bucket', $bucket) ; + $deletes + ->setType(DELETE_TYPE_CACHE_BY_RESOURCE) + ->setResource('file/' . $file->getId()) + ; + $metadata = null; // was causing leaks as it was passed by reference $response->setStatusCode(Response::STATUS_CODE_CREATED); @@ -819,6 +803,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') ->desc('Get File Preview') ->groups(['api', 'storage']) ->label('scope', 'files.read') + ->label('cache', true) + ->label('cache.resource', 'file/{request.fileId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'storage') ->label('sdk.method', 'getFilePreview') @@ -877,14 +863,14 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache $key = \md5($fileId . $width . $height . $gravity . $quality . $borderWidth . $borderColor . $borderRadius . $opacity . $rotation . $background . $output); - $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + $file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } if ($fileSecurity) { - $valid = $validator->isValid($file->getRead()); + $valid |= $validator->isValid($file->getRead()); if (!$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -895,7 +881,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $algorithm = $file->getAttribute('algorithm'); $cipher = $file->getAttribute('openSSLCipher'); $mime = $file->getAttribute('mimeType'); - if (!\in_array($mime, $inputs) || $file->getAttribute('sizeActual') > (int) App::getEnv('_APP_STORAGE_PREVIEW_LIMIT', 20000000)) { if (!\in_array($mime, $inputs)) { $path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default']; @@ -908,7 +893,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $cipher = null; $background = (empty($background)) ? 'eceff1' : $background; $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); - $key = \md5($path . $width . $height . $gravity . $quality . $borderWidth . $borderColor . $borderRadius . $opacity . $rotation . $background . $output); $deviceFiles = $deviceLocal; } @@ -919,23 +903,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } - $cache = new Cache(new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId() . DIRECTORY_SEPARATOR . $bucketId . DIRECTORY_SEPARATOR . $fileId)); // Limit file number or size - $data = $cache->load($key, 60 * 60 * 24 * 30 * 3/* 3 months */); - if (empty($output)) { // when file extension is not provided and the mime type is not one of our supported outputs // we fallback to `jpg` output format $output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type; } - if ($data) { - return $response - ->setContentType((\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg']) - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'hit') - ->send($data) - ; - } $source = $deviceFiles->read($path); @@ -980,20 +953,18 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $data = $image->output($output, $quality); - $cache->save($key, $data); - $usage ->setParam('storage.files.read', 1) ->setParam('bucketId', $bucketId) ; - $response - ->setContentType((\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg']) - ->addHeader('Expires', $date) - ->addHeader('X-Appwrite-Cache', 'miss') - ->send($data) - ; + $contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg']; + $response + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT') + ->setContentType($contentType) + ->file($data) + ; unset($image); }); @@ -1290,6 +1261,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') ->groups(['api', 'storage']) ->label('scope', 'files.write') ->label('event', 'buckets.[bucketId].files.[fileId].update') + ->label('audits.resource', 'files/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'storage') ->label('sdk.method', 'updateFile') @@ -1303,11 +1275,10 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('response') ->inject('dbForProject') ->inject('user') - ->inject('audits') ->inject('usage') ->inject('mode') ->inject('events') - ->action(function (string $bucketId, string $fileId, ?array $permissions, Response $response, Database $dbForProject, Document $user, Audit $audits, Stats $usage, string $mode, Event $events) { + ->action(function (string $bucketId, string $fileId, ?array $permissions, Response $response, Database $dbForProject, Document $user, Stats $usage, string $mode, Event $events) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); @@ -1373,8 +1344,6 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') ->setContext('bucket', $bucket) ; - $audits->setResource('file/' . $file->getId()); - $usage ->setParam('storage.files.update', 1) ->setParam('bucketId', $bucketId) @@ -1389,6 +1358,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') ->groups(['api', 'storage']) ->label('scope', 'files.write') ->label('event', 'buckets.[bucketId].files.[fileId].delete') + ->label('audits.resource', 'file/{request.fileId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'storage') ->label('sdk.method', 'deleteFile') @@ -1400,12 +1370,11 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('response') ->inject('dbForProject') ->inject('events') - ->inject('audits') ->inject('usage') ->inject('mode') ->inject('deviceFiles') - ->inject('project') - ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $events, Audit $audits, Stats $usage, string $mode, Device $deviceFiles, Document $project) { + ->inject('deletes') + ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $events, Stats $usage, string $mode, Device $deviceFiles, Delete $deletes) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) { @@ -1443,10 +1412,10 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') } if ($deviceDeleted) { - //delete related cache - $cacheDir = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId() . DIRECTORY_SEPARATOR . $bucketId . DIRECTORY_SEPARATOR . $fileId; - $deviceLocal = new Local($cacheDir); - $deviceLocal->delete($cacheDir, true); + $deletes + ->setType(DELETE_TYPE_CACHE_BY_RESOURCE) + ->setResource('file/' . $fileId) + ; $deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId); @@ -1457,8 +1426,6 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to delete file from device'); } - $audits->setResource('file/' . $file->getId()); - $usage ->setParam('storage', $file->getAttribute('size', 0) * -1) ->setParam('storage.files.delete', 1) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index d0ce40db32..f20f90049d 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -2,7 +2,6 @@ use Appwrite\Auth\Auth; use Appwrite\Detector\Detector; -use Appwrite\Event\Audit as EventAudit; use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Event\Mail; @@ -41,6 +40,7 @@ App::post('/v1/teams') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].create') ->label('scope', 'teams.write') + ->label('audits.resource', 'team/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'teams') ->label('sdk.method', 'create') @@ -55,8 +55,7 @@ App::post('/v1/teams') ->inject('user') ->inject('dbForProject') ->inject('events') - ->inject('audits') - ->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $events, Event $audits) { + ->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $events) { $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); $isAppUser = Auth::isAppUser(Authorization::getRoles()); @@ -108,12 +107,6 @@ App::post('/v1/teams') $events->setParam('userId', $user->getId()); } - $audits - ->setParam('event', 'teams.create') - ->setParam('resource', 'team/' . $teamId) - ->setParam('data', $team->getArrayCopy()) - ; - $response->setStatusCode(Response::STATUS_CODE_CREATED); $response->dynamic($team, Response::MODEL_TEAM); }); @@ -198,6 +191,7 @@ App::put('/v1/teams/:teamId') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].update') ->label('scope', 'teams.write') + ->label('audits.resource', 'team/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'teams') ->label('sdk.method', 'update') @@ -210,8 +204,7 @@ App::put('/v1/teams/:teamId') ->inject('response') ->inject('dbForProject') ->inject('events') - ->inject('audits') - ->action(function (string $teamId, string $name, Response $response, Database $dbForProject, Event $events, EventAudit $audits) { + ->action(function (string $teamId, string $name, Response $response, Database $dbForProject, Event $events) { $team = $dbForProject->getDocument('teams', $teamId); @@ -224,7 +217,6 @@ App::put('/v1/teams/:teamId') ->setAttribute('search', implode(' ', [$teamId, $name]))); $events->setParam('teamId', $team->getId()); - $audits->setResource('team/' . $team->getId()); $response->dynamic($team, Response::MODEL_TEAM); }); @@ -234,6 +226,7 @@ App::delete('/v1/teams/:teamId') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].delete') ->label('scope', 'teams.write') + ->label('audits.resource', 'team/{request.teamId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'teams') ->label('sdk.method', 'delete') @@ -245,8 +238,7 @@ App::delete('/v1/teams/:teamId') ->inject('dbForProject') ->inject('events') ->inject('deletes') - ->inject('audits') - ->action(function (string $teamId, Response $response, Database $dbForProject, Event $events, Delete $deletes, EventAudit $audits) { + ->action(function (string $teamId, Response $response, Database $dbForProject, Event $events, Delete $deletes) { $team = $dbForProject->getDocument('teams', $teamId); @@ -279,12 +271,6 @@ App::delete('/v1/teams/:teamId') ->setPayload($response->output($team, Response::MODEL_TEAM)) ; - $audits - ->setParam('event', 'teams.delete') - ->setParam('resource', 'team/' . $teamId) - ->setParam('data', $team->getArrayCopy()) - ; - $response->noContent(); }); @@ -294,6 +280,8 @@ App::post('/v1/teams/:teamId/memberships') ->label('event', 'teams.[teamId].memberships.[membershipId].create') ->label('scope', 'teams.write') ->label('auth.type', 'invites') + ->label('audits.resource', 'team/{request.teamId}') + ->label('audits.userId', '{request.userId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'teams') ->label('sdk.method', 'createMembership') @@ -312,10 +300,9 @@ App::post('/v1/teams/:teamId/memberships') ->inject('user') ->inject('dbForProject') ->inject('locale') - ->inject('audits') ->inject('mails') ->inject('events') - ->action(function (string $teamId, string $email, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, EventAudit $audits, Mail $mails, Event $events) { + ->action(function (string $teamId, string $email, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $mails, Event $events) { $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); $isAppUser = Auth::isAppUser(Authorization::getRoles()); @@ -358,7 +345,9 @@ App::post('/v1/teams/:teamId/memberships') 'email' => $email, 'emailVerification' => false, 'status' => true, - 'password' => Auth::passwordHash(Auth::passwordGenerator()), + 'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS), + 'hash' => Auth::DEFAULT_ALGO, + 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, /** * Set the password update time to 0 for users created using * team invite and OAuth to allow password updates without an @@ -444,10 +433,6 @@ App::post('/v1/teams/:teamId/memberships') ; } - $audits - ->setResource('team/' . $teamId) - ; - $events ->setParam('teamId', $team->getId()) ->setParam('membershipId', $membership->getId()) @@ -587,6 +572,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].memberships.[membershipId].update') ->label('scope', 'teams.write') + ->label('audits.resource', 'team/{request.teamId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'teams') ->label('sdk.method', 'updateMembershipRoles') @@ -601,9 +587,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ->inject('response') ->inject('user') ->inject('dbForProject') - ->inject('audits') ->inject('events') - ->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, EventAudit $audits, Event $events) { + ->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $events) { $team = $dbForProject->getDocument('teams', $teamId); if ($team->isEmpty()) { @@ -639,8 +624,6 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') */ $dbForProject->deleteCachedDocument('users', $profile->getId()); - $audits->setResource('team/' . $teamId); - $events ->setParam('teamId', $team->getId()) ->setParam('membershipId', $membership->getId()); @@ -659,6 +642,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].memberships.[membershipId].update.status') ->label('scope', 'public') + ->label('audits.resource', 'team/{request.teamId}') + ->label('audits.userId', '{request.userId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'teams') ->label('sdk.method', 'updateMembershipStatus') @@ -675,9 +660,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->inject('user') ->inject('dbForProject') ->inject('geodb') - ->inject('audits') ->inject('events') - ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Reader $geodb, EventAudit $audits, Event $events) { + ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Reader $geodb, Event $events) { $protocol = $request->getProtocol(); $membership = $dbForProject->getDocument('memberships', $membershipId); @@ -721,9 +705,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->setAttribute('confirm', true) ; - $user - ->setAttribute('emailVerification', true) - ; + $user->setAttribute('emailVerification', true); // Log user in @@ -763,8 +745,6 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') $team = Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team->setAttribute('total', $team->getAttribute('total', 0) + 1))); - $audits->setResource('team/' . $teamId); - $events ->setParam('teamId', $team->getId()) ->setParam('membershipId', $membership->getId()) @@ -795,6 +775,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') ->groups(['api', 'teams']) ->label('event', 'teams.[teamId].memberships.[membershipId].delete') ->label('scope', 'teams.write') + ->label('audits.resource', 'team/{request.teamId}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'teams') ->label('sdk.method', 'deleteMembership') @@ -805,9 +786,8 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') ->param('membershipId', '', new UID(), 'Membership ID.') ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('events') - ->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, EventAudit $audits, Event $events) { + ->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, Event $events) { $membership = $dbForProject->getDocument('memberships', $membershipId); @@ -854,8 +834,6 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team)); } - $audits->setResource('team/' . $teamId); - $events ->setParam('teamId', $team->getId()) ->setParam('membershipId', $membership->getId()) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index bbd0146c8b..172c845f2f 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -6,7 +6,6 @@ use Appwrite\Auth\Validator\Phone; use Appwrite\Detector\Detector; use Appwrite\Event\Delete; use Appwrite\Event\Event; -use Appwrite\Event\Audit as EventAudit; use Appwrite\Network\Validator\Email; use Appwrite\Stats\Stats; use Appwrite\Utopia\Database\Validator\CustomId; @@ -32,12 +31,64 @@ use Utopia\Validator\Text; use Utopia\Validator\Range; use Utopia\Validator\Boolean; use MaxMind\Db\Reader; +use Utopia\Validator\Integer; + +/** TODO: Remove function when we move to using utopia/platform */ +function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Database $dbForProject, Stats $usage, Event $events): Document +{ + $hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array + + if (!empty($email)) { + $email = \strtolower($email); + } + + try { + $userId = $userId == 'unique()' + ? ID::unique() + : ID::custom($userId); + + $user = $dbForProject->createDocument('users', new Document([ + '$id' => $userId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::user($userId)), + Permission::delete(Role::user($userId)), + ], + 'email' => $email, + 'emailVerification' => false, + 'phone' => $phone, + 'phoneVerification' => false, + 'status' => true, + 'password' => (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null, + 'hash' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO : $hash, + 'hashOptions' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO_OPTIONS : $hashOptions, + 'passwordUpdate' => (!empty($password)) ? DateTime::now() : null, + 'registration' => DateTime::now(), + 'reset' => false, + 'name' => $name, + 'prefs' => new \stdClass(), + 'sessions' => null, + 'tokens' => null, + 'memberships' => null, + 'search' => implode(' ', [$userId, $email, $phone, $name]) + ])); + } catch (Duplicate $th) { + throw new Exception(Exception::USER_ALREADY_EXISTS); + } + + $usage->setParam('users.create', 1); + + $events->setParam('userId', $user->getId()); + + return $user; +} App::post('/v1/users') ->desc('Create User') ->groups(['api', 'users']) ->label('event', 'users.[userId].create') ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'users') ->label('sdk.method', 'create') @@ -46,51 +97,235 @@ App::post('/v1/users') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_USER) ->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('email', null, new Email(), 'User email.', true) + ->param('phone', null, new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) + ->param('password', null, new Password(), 'Plain text user password. Must be at least 8 chars.', true) + ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('usage') + ->inject('events') + ->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) { + $user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $dbForProject, $usage, $events); + + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($user, Response::MODEL_USER); + }); + +App::post('/v1/users/bcrypt') + ->desc('Create User with Bcrypt Password') + ->groups(['api', 'users']) + ->label('event', 'users.[userId].create') + ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'createBcryptUser') + ->label('sdk.description', '/docs/references/users/create-bcrypt-user.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('email', '', new Email(), 'User email.') - ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') + ->param('password', '', new Password(), 'User password hashed using Bcrypt.') ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) ->inject('response') ->inject('dbForProject') ->inject('usage') ->inject('events') ->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) { + $user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events); - $email = \strtolower($email); + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($user, Response::MODEL_USER); + }); - try { - $userId = $userId == 'unique()' ? ID::unique() : $userId; - $user = $dbForProject->createDocument('users', new Document([ - '$id' => $userId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::user($userId)), - Permission::delete(Role::user($userId)), - ], - 'email' => $email, - 'emailVerification' => false, - 'status' => true, - 'password' => Auth::passwordHash($password), - 'passwordUpdate' => DateTime::now(), - 'registration' => DateTime::now(), - 'reset' => false, - 'name' => $name, - 'prefs' => new \stdClass(), - 'sessions' => null, - 'tokens' => null, - 'memberships' => null, - 'search' => implode(' ', [$userId, $email, $name]) - ])); - } catch (Duplicate $th) { - throw new Exception(Exception::USER_ALREADY_EXISTS); +App::post('/v1/users/md5') + ->desc('Create User with MD5 Password') + ->groups(['api', 'users']) + ->label('event', 'users.[userId].create') + ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'createMD5User') + ->label('sdk.description', '/docs/references/users/create-md5-user.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('email', '', new Email(), 'User email.') + ->param('password', '', new Password(), 'User password hashed using MD5.') + ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('usage') + ->inject('events') + ->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) { + $user = createUser('md5', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events); + + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($user, Response::MODEL_USER); + }); + +App::post('/v1/users/argon2') + ->desc('Create User with Argon2 Password') + ->groups(['api', 'users']) + ->label('event', 'users.[userId].create') + ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'createArgon2User') + ->label('sdk.description', '/docs/references/users/create-argon2-user.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('email', '', new Email(), 'User email.') + ->param('password', '', new Password(), 'User password hashed using Argon2.') + ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('usage') + ->inject('events') + ->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) { + $user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events); + + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($user, Response::MODEL_USER); + }); + +App::post('/v1/users/sha') + ->desc('Create User with SHA Password') + ->groups(['api', 'users']) + ->label('event', 'users.[userId].create') + ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'createSHAUser') + ->label('sdk.description', '/docs/references/users/create-sha-user.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('email', '', new Email(), 'User email.') + ->param('password', '', new Password(), 'User password hashed using SHA.') + ->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true) + ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('usage') + ->inject('events') + ->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) { + $options = '{}'; + + if (!empty($passwordVersion)) { + $options = '{"version":"' . $passwordVersion . '"}'; } - $usage - ->setParam('users.create', 1) - ; + $user = createUser('sha', $options, $userId, $email, $password, null, $name, $dbForProject, $usage, $events); - $events - ->setParam('userId', $user->getId()) - ; + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($user, Response::MODEL_USER); + }); + +App::post('/v1/users/phpass') + ->desc('Create User with PHPass Password') + ->groups(['api', 'users']) + ->label('event', 'users.[userId].create') + ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'createPHPassUser') + ->label('sdk.description', '/docs/references/users/create-phpass-user.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('email', '', new Email(), 'User email.') + ->param('password', '', new Password(), 'User password hashed using PHPass.') + ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('usage') + ->inject('events') + ->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) { + $user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events); + + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($user, Response::MODEL_USER); + }); + +App::post('/v1/users/scrypt') + ->desc('Create User with Scrypt Password') + ->groups(['api', 'users']) + ->label('event', 'users.[userId].create') + ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'createScryptUser') + ->label('sdk.description', '/docs/references/users/create-scrypt-user.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('email', '', new Email(), 'User email.') + ->param('password', '', new Password(), 'User password hashed using Scrypt.') + ->param('passwordSalt', '', new Text(128), 'Optional salt used to hash password.') + ->param('passwordCpu', '', new Integer(), 'Optional CPU cost used to hash password.') + ->param('passwordMemory', '', new Integer(), 'Optional memory cost used to hash password.') + ->param('passwordParallel', '', new Integer(), 'Optional parallelization cost used to hash password.') + ->param('passwordLength', '', new Integer(), 'Optional hash length used to hash password.') + ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('usage') + ->inject('events') + ->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) { + $options = [ + 'salt' => $passwordSalt, + 'costCpu' => $passwordCpu, + 'costMemory' => $passwordMemory, + 'costParallel' => $passwordParallel, + 'length' => $passwordLength + ]; + + $user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $dbForProject, $usage, $events); + + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($user, Response::MODEL_USER); + }); + +App::post('/v1/users/scrypt-modified') + ->desc('Create User with Scrypt Modified Password') + ->groups(['api', 'users']) + ->label('event', 'users.[userId].create') + ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'createScryptModifiedUser') + ->label('sdk.description', '/docs/references/users/create-scrypt-modified-user.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('email', '', new Email(), 'User email.') + ->param('password', '', new Password(), 'User password hashed using Scrypt Modified.') + ->param('passwordSalt', '', new Text(128), 'Salt used to hash password.') + ->param('passwordSaltSeparator', '', new Text(128), 'Salt separator used to hash password.') + ->param('passwordSignerKey', '', new Text(128), 'Signer key used to hash password.') + ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('usage') + ->inject('events') + ->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) { + $user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events); $response->setStatusCode(Response::STATUS_CODE_CREATED); $response->dynamic($user, Response::MODEL_USER); @@ -138,9 +373,7 @@ App::get('/v1/users') $queries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument); } - $usage - ->setParam('users.read', 1) - ; + $usage->setParam('users.read', 1); $response->dynamic(new Document([ 'users' => $dbForProject->find('users', \array_merge($filterQueries, $queries)), @@ -171,12 +404,48 @@ App::get('/v1/users/:userId') throw new Exception(Exception::USER_NOT_FOUND); } - $usage - ->setParam('users.read', 1) - ; + $usage->setParam('users.read', 1); + $response->dynamic($user, Response::MODEL_USER); }); +App::patch('/v1/users/:userId/prefs') + ->desc('Update User Preferences') + ->groups(['api', 'users']) + ->label('event', 'users.[userId].update.prefs') + ->label('scope', 'users.write') + ->label('audits.resource', 'user/{request.userId}') + ->label('audits.userId', '{request.userId}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'updatePrefs') + ->label('sdk.description', '/docs/references/users/update-user-prefs.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PREFERENCES) + ->param('userId', '', new UID(), 'User ID.') + ->param('prefs', '', new Assoc(), 'Prefs key-value JSON object.') + ->inject('response') + ->inject('dbForProject') + ->inject('usage') + ->inject('events') + ->action(function (string $userId, array $prefs, Response $response, Database $dbForProject, Stats $usage, Event $events) { + + $user = $dbForProject->getDocument('users', $userId); + + if ($user->isEmpty()) { + throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); + } + + $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs)); + + $usage->setParam('users.update', 1); + + $events->setParam('userId', $user->getId()); + + $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); + }); + App::get('/v1/users/:userId/prefs') ->desc('Get User Preferences') ->groups(['api', 'users']) @@ -202,9 +471,8 @@ App::get('/v1/users/:userId/prefs') $prefs = $user->getAttribute('prefs', new \stdClass()); - $usage - ->setParam('users.read', 1) - ; + $usage->setParam('users.read', 1); + $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); @@ -244,9 +512,8 @@ App::get('/v1/users/:userId/sessions') $sessions[$key] = $session; } - $usage - ->setParam('users.read', 1) - ; + $usage->setParam('users.read', 1); + $response->dynamic(new Document([ 'sessions' => $sessions, 'total' => count($sessions), @@ -364,9 +631,7 @@ App::get('/v1/users/:userId/logs') } } - $usage - ->setParam('users.read', 1) - ; + $usage->setParam('users.read', 1); $response->dynamic(new Document([ 'total' => $audit->countLogsByUser($user->getId()), @@ -379,6 +644,8 @@ App::patch('/v1/users/:userId/status') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.status') ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'users') ->label('sdk.method', 'updateStatus') @@ -402,13 +669,9 @@ App::patch('/v1/users/:userId/status') $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', (bool) $status)); - $usage - ->setParam('users.update', 1) - ; + $usage->setParam('users.update', 1); - $events - ->setParam('userId', $user->getId()) - ; + $events->setParam('userId', $user->getId()); $response->dynamic($user, Response::MODEL_USER); }); @@ -418,6 +681,7 @@ App::patch('/v1/users/:userId/verification') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.verification') ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'users') ->label('sdk.method', 'updateEmailVerification') @@ -441,13 +705,9 @@ App::patch('/v1/users/:userId/verification') $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification)); - $usage - ->setParam('users.update', 1) - ; + $usage->setParam('users.update', 1); - $events - ->setParam('userId', $user->getId()) - ; + $events->setParam('userId', $user->getId()); $response->dynamic($user, Response::MODEL_USER); }); @@ -457,6 +717,7 @@ App::patch('/v1/users/:userId/verification/phone') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.verification') ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'users') ->label('sdk.method', 'updatePhoneVerification') @@ -480,13 +741,9 @@ App::patch('/v1/users/:userId/verification/phone') $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification)); - $usage - ->setParam('users.update', 1) - ; + $usage->setParam('users.update', 1); - $events - ->setParam('userId', $user->getId()) - ; + $events->setParam('userId', $user->getId()); $response->dynamic($user, Response::MODEL_USER); }); @@ -496,6 +753,8 @@ App::patch('/v1/users/:userId/name') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.name') ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'users') ->label('sdk.method', 'updateName') @@ -507,9 +766,8 @@ App::patch('/v1/users/:userId/name') ->param('name', '', new Text(128), 'User name. Max length: 128 chars.') ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('events') - ->action(function (string $userId, string $name, Response $response, Database $dbForProject, EventAudit $audits, Event $events) { + ->action(function (string $userId, string $name, Response $response, Database $dbForProject, Event $events) { $user = $dbForProject->getDocument('users', $userId); @@ -524,13 +782,7 @@ App::patch('/v1/users/:userId/name') $user = $dbForProject->updateDocument('users', $user->getId(), $user); - $audits - ->setResource('user/' . $user->getId()) - ; - - $events - ->setParam('userId', $user->getId()) - ; + $events->setParam('userId', $user->getId()); $response->dynamic($user, Response::MODEL_USER); }); @@ -540,6 +792,8 @@ App::patch('/v1/users/:userId/password') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.password') ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'users') ->label('sdk.method', 'updatePassword') @@ -551,9 +805,8 @@ App::patch('/v1/users/:userId/password') ->param('password', '', new Password(), 'New user password. Must be at least 8 chars.') ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('events') - ->action(function (string $userId, string $password, Response $response, Database $dbForProject, EventAudit $audits, Event $events) { + ->action(function (string $userId, string $password, Response $response, Database $dbForProject, Event $events) { $user = $dbForProject->getDocument('users', $userId); @@ -562,18 +815,14 @@ App::patch('/v1/users/:userId/password') } $user - ->setAttribute('password', Auth::passwordHash($password)) + ->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS)) + ->setAttribute('hash', Auth::DEFAULT_ALGO) + ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS) ->setAttribute('passwordUpdate', DateTime::now()); $user = $dbForProject->updateDocument('users', $user->getId(), $user); - $audits - ->setResource('user/' . $user->getId()) - ; - - $events - ->setParam('userId', $user->getId()) - ; + $events->setParam('userId', $user->getId()); $response->dynamic($user, Response::MODEL_USER); }); @@ -583,6 +832,8 @@ App::patch('/v1/users/:userId/email') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.email') ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'users') ->label('sdk.method', 'updateEmail') @@ -594,9 +845,8 @@ App::patch('/v1/users/:userId/email') ->param('email', '', new Email(), 'User email.') ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('events') - ->action(function (string $userId, string $email, Response $response, Database $dbForProject, EventAudit $audits, Event $events) { + ->action(function (string $userId, string $email, Response $response, Database $dbForProject, Event $events) { $user = $dbForProject->getDocument('users', $userId); @@ -618,14 +868,7 @@ App::patch('/v1/users/:userId/email') throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } - - $audits - ->setResource('user/' . $user->getId()) - ; - - $events - ->setParam('userId', $user->getId()) - ; + $events->setParam('userId', $user->getId()); $response->dynamic($user, Response::MODEL_USER); }); @@ -635,6 +878,7 @@ App::patch('/v1/users/:userId/phone') ->groups(['api', 'users']) ->label('event', 'users.[userId].update.phone') ->label('scope', 'users.write') + ->label('audits.resource', 'user/{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'users') ->label('sdk.method', 'updatePhone') @@ -646,9 +890,8 @@ App::patch('/v1/users/:userId/phone') ->param('number', '', new Phone(), 'User phone number.') ->inject('response') ->inject('dbForProject') - ->inject('audits') ->inject('events') - ->action(function (string $userId, string $number, Response $response, Database $dbForProject, EventAudit $audits, Event $events) { + ->action(function (string $userId, string $number, Response $response, Database $dbForProject, Event $events) { $user = $dbForProject->getDocument('users', $userId); @@ -668,14 +911,44 @@ App::patch('/v1/users/:userId/phone') throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } + $events->setParam('userId', $user->getId()); - $audits - ->setResource('user/' . $user->getId()) - ; + $response->dynamic($user, Response::MODEL_USER); + }); - $events - ->setParam('userId', $user->getId()) - ; +App::patch('/v1/users/:userId/verification') + ->desc('Update Email Verification') + ->groups(['api', 'users']) + ->label('event', 'users.[userId].update.verification') + ->label('scope', 'users.write') + ->label('audits.resource', 'user/{request.userId}') + ->label('audits.userId', '{request.userId}') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'updateEmailVerification') + ->label('sdk.description', '/docs/references/users/update-user-email-verification.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->param('userId', '', new UID(), 'User ID.') + ->param('emailVerification', false, new Boolean(), 'User email verification status.') + ->inject('response') + ->inject('dbForProject') + ->inject('usage') + ->inject('events') + ->action(function (string $userId, bool $emailVerification, Response $response, Database $dbForProject, Stats $usage, Event $events) { + + $user = $dbForProject->getDocument('users', $userId); + + if ($user->isEmpty()) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification)); + + $usage->setParam('users.update', 1); + + $events->setParam('userId', $user->getId()); $response->dynamic($user, Response::MODEL_USER); }); @@ -708,13 +981,9 @@ App::patch('/v1/users/:userId/prefs') $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs)); - $usage - ->setParam('users.update', 1) - ; + $usage->setParam('users.update', 1); - $events - ->setParam('userId', $user->getId()) - ; + $events->setParam('userId', $user->getId()); $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); @@ -724,6 +993,7 @@ App::delete('/v1/users/:userId/sessions/:sessionId') ->groups(['api', 'users']) ->label('event', 'users.[userId].sessions.[sessionId].delete') ->label('scope', 'users.write') + ->label('audits.resource', 'user/{request.userId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'users') ->label('sdk.method', 'deleteSession') @@ -772,6 +1042,7 @@ App::delete('/v1/users/:userId/sessions') ->groups(['api', 'users']) ->label('event', 'users.[userId].sessions.[sessionId].delete') ->label('scope', 'users.write') + ->label('audits.resource', 'user/{user.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'users') ->label('sdk.method', 'deleteSessions') @@ -818,6 +1089,7 @@ App::delete('/v1/users/:userId') ->groups(['api', 'users']) ->label('event', 'users.[userId].delete') ->label('scope', 'users.write') + ->label('audits.resource', 'user/{request.userId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'users') ->label('sdk.method', 'delete') @@ -853,9 +1125,7 @@ App::delete('/v1/users/:userId') ->setPayload($response->output($clone, Response::MODEL_USER)) ; - $usage - ->setParam('users.delete', 1) - ; + $usage->setParam('users.delete', 1); $response->noContent(); }); diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 559d29e17e..031ea948d8 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -14,10 +14,37 @@ use Utopia\App; use Appwrite\Extend\Exception; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; +use Utopia\Cache\Adapter\Filesystem; +use Utopia\Cache\Cache; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; -use Utopia\Registry\Registry; + +$parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) { + preg_match_all('/{(.*?)}/', $label, $matches); + foreach ($matches[1] ?? [] as $pos => $match) { + $find = $matches[0][$pos]; + $parts = explode('.', $match); + + if (count($parts) !== 2) { + throw new Exception('Too less or too many parts', 400, Exception::GENERAL_ARGUMENT_INVALID); + } + + $namespace = $parts[0] ?? ''; + $replace = $parts[1] ?? ''; + + $params = match ($namespace) { + 'user' => (array)$user, + 'request' => $requestParams, + default => $responsePayload, + }; + + if (array_key_exists($replace, $params)) { + $label = \str_replace($find, $params[$replace], $label); + } + } + return $label; +}; App::init() ->groups(['api']) @@ -42,9 +69,9 @@ App::init() throw new Exception(Exception::PROJECT_UNKNOWN); } - /* - * Abuse Check - */ + /* + * Abuse Check + */ $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}'); $timeLimitArray = []; @@ -89,7 +116,7 @@ App::init() if ( (App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled' // Route is rate-limited - && $abuse->check()) // Abuse is not disabled + && $abuse->check()) // Abuse is not disabled && (!$isAppUser && !$isPrivilegedUser) ) { // User is not an admin or API key throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED); @@ -100,15 +127,13 @@ App::init() * Background Jobs */ $events - ->setEvent($route->getLabel('event', '')) - ->setProject($project) - ->setUser($user) - ; + ->setEvent($route->getLabel('event', '')) + ->setProject($project) + ->setUser($user); $mails ->setProject($project) - ->setUser($user) - ; + ->setUser($user); $audits ->setMode($mode) @@ -116,8 +141,7 @@ App::init() ->setIP($request->getIP()) ->setEvent($route->getLabel('event', '')) ->setProject($project) - ->setUser($user) - ; + ->setUser($user); $usage ->setParam('projectId', $project->getId()) @@ -127,11 +151,35 @@ App::init() ->setParam('httpPath', $route->getPath()) ->setParam('networkRequestSize', 0) ->setParam('networkResponseSize', 0) - ->setParam('storage', 0) - ; + ->setParam('storage', 0); $deletes->setProject($project); $database->setProject($project); + + $useCache = $route->getLabel('cache', false); + + if ($useCache) { + $key = md5($request->getURI() . implode('*', $request->getParams())); + $cache = new Cache( + new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId()) + ); + $timestamp = 60 * 60 * 24 * 30; + $data = $cache->load($key, $timestamp); + if (!empty($data)) { + $data = json_decode($data, true); + + $response + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $timestamp) . ' GMT') + ->addHeader('X-Appwrite-Cache', 'hit') + ->setContentType($data['content-type']) + ->send(base64_decode($data['payload'])) + ; + + $route->setIsActive(false); + } else { + $response->addHeader('X-Appwrite-Cache', 'miss'); + } + } }); App::init() @@ -201,11 +249,13 @@ App::shutdown() ->inject('database') ->inject('mode') ->inject('dbForProject') - ->action(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) { + ->action(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) use ($parseLabel) { + + $responsePayload = $response->getPayload(); if (!empty($events->getEvent())) { if (empty($events->getPayload())) { - $events->setPayload($response->getPayload()); + $events->setPayload($responsePayload); } /** * Trigger functions. @@ -235,7 +285,7 @@ App::shutdown() $bucket = $events->getContext('bucket'); $target = Realtime::fromPayload( - // Pass first, most verbose event pattern + // Pass first, most verbose event pattern event: $allEvents[0], payload: $payload, project: $project, @@ -258,7 +308,38 @@ App::shutdown() } } - if (!empty($audits->getResource())) { + $route = $utopia->match($request); + $requestParams = $route->getParamsValues(); + $user = $audits->getUser(); + + /** + * Audit labels + */ + $pattern = $route->getLabel('audits.resource', null); + if (!empty($pattern)) { + $resource = $parseLabel($pattern, $responsePayload, $requestParams, $user); + if (!empty($resource) && $resource !== $pattern) { + $audits->setResource($resource); + } + } + + $pattern = $route->getLabel('audits.userId', null); + if (!empty($pattern)) { + $userId = $parseLabel($pattern, $responsePayload, $requestParams, $user); + $user = $dbForProject->getDocument('users', $userId); + $audits->setUser($user); + } + + if (!empty($audits->getResource()) && !empty($audits->getUser()->getId())) { + /** + * audits.payload is switched to default true + * in order to auto audit payload for all endpoints + */ + $pattern = $route->getLabel('audits.payload', true); + if (!empty($pattern)) { + $audits->setPayload($responsePayload); + } + foreach ($events->getParams() as $key => $value) { $audits->setParam($key, $value); } @@ -273,16 +354,58 @@ App::shutdown() $database->trigger(); } - $route = $utopia->match($request); + /** + * Cache label + */ + $useCache = $route->getLabel('cache', false); + if ($useCache) { + $resource = null; + $data = $response->getPayload(); + if (!empty($data['payload'])) { + $pattern = $route->getLabel('cache.resource', null); + if (!empty($pattern)) { + $resource = $parseLabel($pattern, $responsePayload, $requestParams, $user); + } + + $key = md5($request->getURI() . implode('*', $request->getParams())); + + $data = json_encode([ + 'content-type' => $response->getContentType(), + 'payload' => base64_encode($data['payload']), + ]) ; + + $signature = md5($data); + $cacheLog = $dbForProject->getDocument('cache', $key); + if ($cacheLog->isEmpty()) { + Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([ + '$id' => $key, + 'resource' => $resource, + 'accessedAt' => \time(), + 'signature' => $signature, + ]))); + } elseif (date('Y/m/d', \time()) > date('Y/m/d', $cacheLog->getAttribute('accessedAt'))) { + $cacheLog->setAttribute('accessedAt', \time()); + Authorization::skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog)); + } + + if ($signature !== $cacheLog->getAttribute('signature')) { + $cache = new Cache( + new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId()) + ); + $cache->save($key, $data); + } + } + } + if ( App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled' && $project->getId() && $mode !== APP_MODE_ADMIN // TODO: add check to make sure user is admin && !empty($route->getLabel('sdk.namespace', null)) - ) { // Don't calculate console usage on admin mode + ) { $usage - ->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage')) - ->setParam('networkResponseSize', $response->getSize()) - ->submit(); + ->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage')) + ->setParam('networkResponseSize', $response->getSize()) + ->submit(); } }); diff --git a/app/executor.php b/app/executor.php index 6791a02889..d3ef55d9ae 100644 --- a/app/executor.php +++ b/app/executor.php @@ -494,6 +494,7 @@ App::post('/v1/execution') $executionStart = \microtime(true); $stdout = ''; $stderr = ''; + $res = ''; $statusCode = 0; $errNo = -1; $executorResponse = ''; @@ -521,6 +522,7 @@ App::post('/v1/execution') ]); $executorResponse = \curl_exec($ch); + $executorResponse = json_decode($executorResponse, true); $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); @@ -544,13 +546,19 @@ App::post('/v1/execution') switch (true) { case $statusCode >= 500: - $stderr = $executorResponse ?? 'Internal Runtime error.'; + $stderr = ($executorResponse ?? [])['stderr'] ?? 'Internal Runtime error.'; + $stdout = ($executorResponse ?? [])['stdout'] ?? 'Internal Runtime error.'; break; case $statusCode >= 100: - $stdout = $executorResponse; + $stdout = $executorResponse['stdout']; + $res = $executorResponse['response']; + if (is_array($res)) { + $res = json_encode($res, JSON_UNESCAPED_UNICODE); + } break; default: - $stderr = $executorResponse ?? 'Execution failed.'; + $stderr = ($executorResponse ?? [])['stderr'] ?? 'Execution failed.'; + $stdout = ($executorResponse ?? [])['stdout'] ?? ''; break; } @@ -563,7 +571,8 @@ App::post('/v1/execution') $execution = [ 'status' => $functionStatus, 'statusCode' => $statusCode, - 'response' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB + 'response' => \mb_strcut($res, 0, 1000000), // Limit to 1MB + 'stdout' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB 'stderr' => \mb_strcut($stderr, 0, 1000000), // Limit to 1MB 'time' => $executionTime, ]; @@ -654,7 +663,7 @@ $http->on('start', function ($http) { /** * Warmup: make sure images are ready to run fast 🚀 */ - $runtimes = new Runtimes('v1'); + $runtimes = new Runtimes('v2'); $allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES')); $runtimes = $runtimes->getAll(true, $allowList); foreach ($runtimes as $runtime) { diff --git a/app/init.php b/app/init.php index 6145ad3f87..82d255f55d 100644 --- a/app/init.php +++ b/app/init.php @@ -23,12 +23,12 @@ use Ahc\Jwt\JWT; use Ahc\Jwt\JWTException; use Appwrite\Extend\Exception; use Appwrite\Auth\Auth; -use Appwrite\Auth\Phone\Mock; -use Appwrite\Auth\Phone\Telesign; -use Appwrite\Auth\Phone\TextMagic; -use Appwrite\Auth\Phone\Twilio; -use Appwrite\Auth\Phone\Msg91; -use Appwrite\Auth\Phone\Vonage; +use Appwrite\SMS\Adapter\Mock; +use Appwrite\SMS\Adapter\Telesign; +use Appwrite\SMS\Adapter\TextMagic; +use Appwrite\SMS\Adapter\Twilio; +use Appwrite\SMS\Adapter\Msg91; +use Appwrite\SMS\Adapter\Vonage; use Appwrite\DSN\DSN; use Appwrite\Event\Audit; use Appwrite\Event\Database as EventDatabase; @@ -146,6 +146,8 @@ const DELETE_TYPE_USAGE = 'usage'; const DELETE_TYPE_REALTIME = 'realtime'; const DELETE_TYPE_BUCKETS = 'buckets'; const DELETE_TYPE_SESSIONS = 'sessions'; +const DELETE_TYPE_CACHE_BY_TIMESTAMP = 'cacheByTimeStamp'; +const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource'; // Mail Types const MAIL_TYPE_VERIFICATION = 'verification'; const MAIL_TYPE_MAGIC_SESSION = 'magicSession'; @@ -998,8 +1000,8 @@ App::setResource('geodb', function ($register) { return $register->get('geodb'); }, ['register']); -App::setResource('phone', function () { - $dsn = new DSN(App::getEnv('_APP_PHONE_PROVIDER')); +App::setResource('sms', function () { + $dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER')); $user = $dsn->getUser(); $secret = $dsn->getPassword(); diff --git a/app/realtime.php b/app/realtime.php index ce637e1448..be87c3d6e6 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -434,7 +434,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $realtime->subscribe($project->getId(), $connection, $roles, $channels); - $user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_USER); + $user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT); $server->send([$connection], json_encode([ 'type' => 'connected', @@ -549,7 +549,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re $channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId()); $realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels); - $user = $response->output($user, Response::MODEL_USER); + $user = $response->output($user, Response::MODEL_ACCOUNT); $server->send([$connection], json_encode([ 'type' => 'response', 'data' => [ diff --git a/app/tasks/maintenance.php b/app/tasks/maintenance.php index e5d6eb3755..c0fa920787 100644 --- a/app/tasks/maintenance.php +++ b/app/tasks/maintenance.php @@ -129,6 +129,15 @@ $cli } } + function notifyDeleteCache($interval) + { + + (new Delete()) + ->setType(DELETE_TYPE_CACHE_BY_TIMESTAMP) + ->setTimestamp(time() - $interval) + ->trigger(); + } + // # of days in seconds (1 day = 86400s) $interval = (int) App::getEnv('_APP_MAINTENANCE_INTERVAL', '86400'); $executionLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', '1209600'); @@ -136,8 +145,9 @@ $cli $abuseLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', '86400'); $usageStatsRetention30m = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_30M', '129600'); //36 hours $usageStatsRetention1d = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_1D', '8640000'); // 100 days + $cacheRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_CACHE', '2592000'); // 30 days - Console::loop(function () use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) { + Console::loop(function () use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d, $cacheRetention) { $database = getConsoleDB(); $time = DateTime::now(); @@ -150,5 +160,6 @@ $cli notifyDeleteConnections(); notifyDeleteExpiredSessions(); renewCertificates($database); + notifyDeleteCache($cacheRetention); }, $interval); }); diff --git a/app/views/console/functions/function.phtml b/app/views/console/functions/function.phtml index d465b83d2a..27d4c58eac 100644 --- a/app/views/console/functions/function.phtml +++ b/app/views/console/functions/function.phtml @@ -392,10 +392,10 @@ sort($patterns); Created - Status - Trigger - Runtime - + Status + Trigger + Runtime + @@ -416,29 +416,44 @@ sort($patterns); - + - -
+
+ + + - - + + + - - - -