diff --git a/.travis.yml b/.travis.yml index 92baeb0fb5..b82fc05c1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ script: - docker-compose logs appwrite-worker-functions - docker-compose exec appwrite doctor - docker-compose exec appwrite vars -- docker-compose exec appwrite test +- docker-compose exec appwrite test --debug deploy: - provider: script diff --git a/CHANGES.md b/CHANGES.md index a9b694a825..be68c21448 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,11 @@ ## Features -- Anonymous login +- Added Anonymous Login ([RFC-010](https://github.com/appwrite/rfc/blob/main/010-anonymous-login.md), #914) +- Added new Environment Variable to enable or disable Anonymous Login +- Added events for functions and executions (#971) +- Splited token & session models to become 2 different internal entities (#922) +- Added Dart 2.12 as a new Cloud Functions runtime (#989) - Added option to disable email/password - Added option to disable anonymous login (need to merge and apply changed) - Added option to disable JWT auth @@ -10,16 +14,46 @@ - Option to limit number of users (good for app launches + god account PR) - Added 2 new endpoints to the projects API to allow new settings - Enabled 501 errors (Not Implemented) from the error handler +## Bugs -# Version 0.7.1 (Not Released Yet) +- Fixed default value for HTTPS force option + +## Breaking Changes + +- Only logged in users can execute functions (for guests, use anonymous login) +- Only the user who has triggered the execution get access to the relevant execution logs +- Function execution env `APPWRITE_FUNCTION_EVENT_PAYLOAD` renamed to `APPWRITE_FUNCTION_EVENT_DATA` + +# Version 0.7.2 ## Features -- Better error logs on appwrite cretificates worker +- When creating new resources from the client API, the current user gets both read & write permissions by default. (#1007) +- Added timestamp to errors logs on the HTTP API container (#1002) +- Added verbose tests output on the terminal and CI (#1006) + +## Upgrades + +- Upgraded utopia-php/abuse to version 0.4.0 +- Upgraded utopia-php/analytics to version 0.2.0 + +## Bugs + +- Fixed certificates worker error on successful operations (#1010) +- Fixed head requests not responding (#998) +- Fixed bug when using auth credential for the Redis container (#993) +- Fixed server warning logs on 3** redirect endpoints (#1013) + +# Version 0.7.1 + +## Features + +- Better error logs on appwrite certificates worker - Added option for Redis authentication - Force adding a security email on setup - SMTP is now disabled by default, no dummy SMTP is included in setup - Added a new endpoint that returns the server and SDKs latest versions numbers #941 +- Custom data strings, userId, and JWT available for cloud functions #967 ## Upgrades @@ -31,10 +65,16 @@ - Upgraded influxdb/influxdb-php lib to version 1.15.2 - Upgraded phpmailer/phpmailer lib to version 6.3.0 - Upgraded adhocore/jwt lib to version 1.1.2 +- Upgraded domnikl/statsd to slickdeals/statsd version 3.0 ## Bug Fixes - Updated missing storage env vars +- Fixed a bug, that added a wrong timzone offset to user log timestamps +- Fixed a bug, that Response format header was not added in the access-control-allow-header list. +- Fixed a bug where countryName is unknown on sessions (#933) +- Added missing event users.update.prefs (#952) +- Fixed bug not allowing to reset document permissions (#977) ## Security diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd7217a9af..f2f1bc02a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,7 +92,7 @@ After finishing the installation process, you can start writing and editing code #### Advanced Topics -We love to create issues that are good for begginers and label them as `good for begginers` or `hacktoberfest`, but some more advanced topics might require extra knowledge. Below is a list of links you can use to learn more about some of the more advance topics that will help you master the Appwrite codebase. +We love to create issues that are good for beginners and label them as `good first issue` or `hacktoberfest`, but some more advanced topics might require extra knowledge. Below is a list of links you can use to learn more about some of the more advance topics that will help you master the Appwrite codebase. ##### Tools and Libs - [Docker](https://www.docker.com/get-started) @@ -365,7 +365,7 @@ From time to time, our team will add tutorials that will help contributors find * [Adding Support for a New OAuth2 Provider](./docs/tutorials/add-oauth2-provider.md) * [Appwrite Environment Variables](./docs/tutorials/environment-variables.md) * [Running in Production](./docs/tutorials/running-in-production.md) - +* [Adding Storage Adapter](./docs/tutorials/add-storage-adapter.md) ## Other Ways to Help diff --git a/README.md b/README.md index 950c5a3010..85cff2e6ff 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Appwrite is an end-to-end backend server for Web, Mobile, Native, or Backend apps packaged as a set of Docker microservices. Appwrite abstracts the complexity and repetitiveness required to build a modern backend API from scratch and allows you to build secure apps faster. -Using Appwrite, you can easily integrate your app with user authentication & multiple sign-in methods, a database for storing and querying users and team data, storage and file management, image manipulation, schedule CRON tasks, and [more services](https://appwrite.io/docs). +Using Appwrite, you can easily integrate your app with user authentication & multiple sign-in methods, a database for storing and querying users and team data, storage and file management, image manipulation, Cloud Functions, and [more services](https://appwrite.io/docs). ![Appwrite](public/images/github.png) @@ -53,7 +53,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:0.7.0 + appwrite/appwrite:0.8.0 ``` ### Windows @@ -65,7 +65,7 @@ docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --entrypoint="install" ^ - appwrite/appwrite:0.7.0 + appwrite/appwrite:0.8.0 ``` #### PowerShell @@ -75,13 +75,13 @@ docker run -it --rm , --volume /var/run/docker.sock:/var/run/docker.sock , --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw , --entrypoint="install" , - appwrite/appwrite:0.7.0 + appwrite/appwrite:0.8.0 ``` Once the Docker installation completes, go to http://localhost to access the Appwrite console from your browser. Please note that on non-linux native hosts, the server might take a few minutes to start after installation completes. -For advanced production and custom installation, check out our Docker [environment variables](docs/tutorials/environment-variables.md) docs. You can also use our public [docker-compose.yml](https://appwrite.io/docker-compose.yml) file to manually set up an environment. +For advanced production and custom installation, check out our Docker [environment variables](https://appwrite.io/docs/environment-variables) docs. You can also use our public [docker-compose.yml](https://gist.github.com/eldadfux/977869ff6bdd7312adfd4e629ee15cc5#file-docker-compose-yml) file to manually set up an environment. ### Upgrade from an Older Version diff --git a/app/config/collections.php b/app/config/collections.php index fdd3051c2d..09b27490c8 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -205,7 +205,7 @@ $collections = [ 'key' => 'email', 'type' => Database::SYSTEM_VAR_TYPE_EMAIL, 'default' => '', - 'required' => true, + 'required' => false, 'array' => false, ], [ @@ -223,7 +223,7 @@ $collections = [ 'key' => 'password', 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', - 'required' => true, + 'required' => false, 'array' => false, ], [ @@ -272,6 +272,16 @@ $collections = [ 'required' => true, 'array' => false, ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Sessions', + 'key' => 'sessions', + 'type' => Database::SYSTEM_VAR_TYPE_DOCUMENT, + 'default' => [], + 'required' => false, + 'array' => true, + 'list' => [Database::SYSTEM_COLLECTION_SESSIONS], + ], [ '$collection' => Database::SYSTEM_COLLECTION_RULES, 'label' => 'Tokens', @@ -294,11 +304,11 @@ $collections = [ ], ], ], - Database::SYSTEM_COLLECTION_TOKENS => [ + Database::SYSTEM_COLLECTION_SESSIONS => [ '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, - '$id' => Database::SYSTEM_COLLECTION_TOKENS, + '$id' => Database::SYSTEM_COLLECTION_SESSIONS, '$permissions' => ['read' => ['*']], - 'name' => 'Token', + 'name' => 'Session', 'structure' => true, 'rules' => [ [ @@ -307,16 +317,34 @@ $collections = [ 'key' => 'userId', 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => null, + 'required' => true, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Provider', + 'key' => 'provider', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => true, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Provider User Identifier', + 'key' => 'providerUid', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', 'required' => false, 'array' => false, ], [ '$collection' => Database::SYSTEM_COLLECTION_RULES, - 'label' => 'Type', - 'key' => 'type', - 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, - 'default' => null, - 'required' => true, + 'label' => 'Provider Token', + 'key' => 'providerToken', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => false, 'array' => false, ], [ @@ -474,6 +502,69 @@ $collections = [ ], ], ], + Database::SYSTEM_COLLECTION_TOKENS => [ + '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, + '$id' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['*']], + 'name' => 'Token', + 'structure' => true, + 'rules' => [ + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'User ID', + 'key' => 'userId', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => null, + 'required' => false, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Type', + 'key' => 'type', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, + 'default' => null, + 'required' => true, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Secret', + 'key' => 'secret', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => true, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'Expire', + 'key' => 'expire', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, + 'default' => 0, + 'required' => true, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'User Agent', + 'key' => 'userAgent', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'default' => '', + 'required' => true, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_RULES, + 'label' => 'IP', + 'key' => 'ip', + 'type' => Database::SYSTEM_VAR_TYPE_IP, + 'default' => '', + 'required' => true, + 'array' => false, + ], + ], + ], Database::SYSTEM_COLLECTION_MEMBERSHIPS => [ '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, '$id' => Database::SYSTEM_COLLECTION_MEMBERSHIPS, @@ -1626,26 +1717,6 @@ foreach ($providers as $index => $provider) { 'array' => false, 'filter' => ['encrypt'], ]; - - $collections[Database::SYSTEM_COLLECTION_USERS]['rules'][] = [ - '$collection' => Database::SYSTEM_COLLECTION_RULES, - 'label' => 'OAuth2 '.\ucfirst($index).' ID', - 'key' => 'oauth2'.\ucfirst($index), - 'type' => Database::SYSTEM_VAR_TYPE_TEXT, - 'default' => '', - 'required' => false, - 'array' => false, - ]; - - $collections[Database::SYSTEM_COLLECTION_USERS]['rules'][] = [ - '$collection' => Database::SYSTEM_COLLECTION_RULES, - 'label' => 'OAuth2 '.\ucfirst($index).' Access Token', - 'key' => 'oauth2'.\ucfirst($index).'AccessToken', - 'type' => Database::SYSTEM_VAR_TYPE_TEXT, - 'default' => '', - 'required' => false, - 'array' => false, - ]; } foreach ($auth as $index => $method) { diff --git a/app/config/events.php b/app/config/events.php index 2bb3862fdf..cb0df42a06 100644 --- a/app/config/events.php +++ b/app/config/events.php @@ -97,6 +97,46 @@ return [ 'model' => Response::MODEL_ANY, 'note' => '', ], + 'functions.create' => [ + 'description' => 'This event triggers when a function is created.', + 'model' => Response::MODEL_FUNCTION, + 'note' => 'version >= 0.7', + ], + 'functions.update' => [ + 'description' => 'This event triggers when a function is updated.', + 'model' => Response::MODEL_FUNCTION, + 'note' => 'version >= 0.7', + ], + 'functions.delete' => [ + 'description' => 'This event triggers when a function is deleted.', + 'model' => Response::MODEL_ANY, + 'note' => 'version >= 0.7', + ], + 'functions.tags.create' => [ + 'description' => 'This event triggers when a function tag is created.', + 'model' => Response::MODEL_TAG, + 'note' => 'version >= 0.7', + ], + 'functions.tags.update' => [ + 'description' => 'This event triggers when a function tag is updated.', + 'model' => Response::MODEL_FUNCTION, + 'note' => 'version >= 0.7', + ], + 'functions.tags.delete' => [ + 'description' => 'This event triggers when a function tag is deleted.', + 'model' => Response::MODEL_ANY, + 'note' => 'version >= 0.7', + ], + 'functions.executions.create' => [ + 'description' => 'This event triggers when a function execution is created.', + 'model' => Response::MODEL_EXECUTION, + 'note' => 'version >= 0.7', + ], + 'functions.executions.update' => [ + 'description' => 'This event triggers when a function execution is updated.', + 'model' => Response::MODEL_EXECUTION, + 'note' => 'version >= 0.7', + ], 'storage.files.create' => [ 'description' => 'This event triggers when a storage file is created.', 'model' => Response::MODEL_FILE, @@ -117,6 +157,11 @@ return [ 'model' => Response::MODEL_USER, 'note' => 'version >= 0.7', ], + 'users.update.prefs' => [ + 'description' => 'This event triggers when a user preference is updated from the users API.', + 'model' => Response::MODEL_ANY, + 'note' => 'version >= 0.7', + ], 'users.update.status' => [ 'description' => 'This event triggers when a user status is updated from the users API.', 'model' => Response::MODEL_USER, @@ -162,4 +207,4 @@ return [ 'model' => Response::MODEL_MEMBERSHIP, 'note' => 'version >= 0.7', ], -]; \ No newline at end of file +]; diff --git a/app/config/platforms.php b/app/config/platforms.php index 399d52e74b..0b65f2675c 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -32,7 +32,7 @@ return [ [ 'key' => 'flutter', 'name' => 'Flutter', - 'version' => '0.3.0', + 'version' => '0.4.0', 'url' => 'https://github.com/appwrite/sdk-for-flutter', 'package' => 'https://pub.dev/packages/appwrite', 'enabled' => true, @@ -165,7 +165,7 @@ return [ [ 'key' => 'deno', 'name' => 'Deno', - 'version' => '0.1.0', + 'version' => '0.1.1', 'url' => 'https://github.com/appwrite/sdk-for-deno', 'package' => 'https://deno.land/x/appwrite', 'enabled' => true, @@ -199,7 +199,7 @@ return [ [ 'key' => 'python', 'name' => 'Python', - 'version' => '0.1.0', + 'version' => '0.1.1', 'url' => 'https://github.com/appwrite/sdk-for-python', 'package' => 'https://pypi.org/project/appwrite/', 'enabled' => true, @@ -216,7 +216,7 @@ return [ [ 'key' => 'ruby', 'name' => 'Ruby', - 'version' => '2.0.0', + 'version' => '2.0.2', 'url' => 'https://github.com/appwrite/sdk-for-ruby', 'package' => 'https://rubygems.org/gems/appwrite', 'enabled' => true, @@ -284,12 +284,12 @@ return [ [ 'key' => 'dart', 'name' => 'Dart', - 'version' => '0.2.0', + 'version' => '0.4.0', 'url' => 'https://github.com/appwrite/sdk-for-dart', 'package' => 'https://pub.dev/packages/dart_appwrite', 'enabled' => true, 'beta' => true, - 'dev' => true, + 'dev' => false, 'hidden' => false, 'family' => APP_PLATFORM_SERVER, 'prism' => 'dart', @@ -301,7 +301,7 @@ return [ [ 'key' => 'cli', 'name' => 'Command Line', - 'version' => '0.5.0', + 'version' => '0.6.0', 'url' => 'https://github.com/appwrite/sdk-for-cli', 'package' => 'https://github.com/appwrite/sdk-for-cli', 'enabled' => true, diff --git a/app/config/roles.php b/app/config/roles.php index 78dd24ad45..3e06ddbfde 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -60,8 +60,6 @@ return [ 'files.read', 'locale.read', 'avatars.read', - 'execution.read', - 'execution.write', ], ], Auth::USER_ROLE_MEMBER => [ diff --git a/app/config/specs/0.6.2.client.json b/app/config/specs/0.6.x.client.json similarity index 100% rename from app/config/specs/0.6.2.client.json rename to app/config/specs/0.6.x.client.json diff --git a/app/config/specs/0.6.2.console.json b/app/config/specs/0.6.x.console.json similarity index 100% rename from app/config/specs/0.6.2.console.json rename to app/config/specs/0.6.x.console.json diff --git a/app/config/specs/0.6.2.server.json b/app/config/specs/0.6.x.server.json similarity index 100% rename from app/config/specs/0.6.2.server.json rename to app/config/specs/0.6.x.server.json diff --git a/app/config/specs/0.7.0.client.json b/app/config/specs/0.7.x.client.json similarity index 100% rename from app/config/specs/0.7.0.client.json rename to app/config/specs/0.7.x.client.json diff --git a/app/config/specs/0.7.0.console.json b/app/config/specs/0.7.x.console.json similarity index 100% rename from app/config/specs/0.7.0.console.json rename to app/config/specs/0.7.x.console.json diff --git a/app/config/specs/0.7.0.server.json b/app/config/specs/0.7.x.server.json similarity index 100% rename from app/config/specs/0.7.0.server.json rename to app/config/specs/0.7.x.server.json diff --git a/app/config/variables.php b/app/config/variables.php index 84b3288348..02738821a1 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -27,7 +27,7 @@ return [ 'name' => '_APP_OPTIONS_FORCE_HTTPS', 'description' => 'Allows you to force HTTPS connection to your API. This feature redirects any HTTP call to HTTPS and adds the \'Strict-Transport-Security\' header to all HTTP responses. By default, set to \'enabled\'. To disable, set to \'disabled\'. This feature will work only when your ports are set to default 80 and 443.', 'introduction' => '', - 'default' => 'enabled', + 'default' => 'disabled', 'required' => false, 'question' => '', ], @@ -107,8 +107,8 @@ return [ 'name' => '_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', 'description' => 'This is the email address used to issue SSL certificates for custom domains or the user agent in your webhooks payload.', 'introduction' => '0.7.0', - 'default' => '', - 'required' => true, + 'default' => 'certs@appwrite.io', + 'required' => false, 'question' => '', ], [ @@ -118,7 +118,7 @@ return [ 'default' => 'enabled', 'required' => false, 'question' => '', - ], + ] ], ], [ @@ -143,7 +143,7 @@ return [ ], [ 'name' => '_APP_REDIS_USER', - 'description' => 'Redis server user.', + 'description' => 'Redis server user. This is an optional variable. Default value is an empty string.', 'introduction' => '0.7', 'default' => '', 'required' => false, @@ -151,7 +151,7 @@ return [ ], [ 'name' => '_APP_REDIS_PASS', - 'description' => 'Redis server password.', + 'description' => 'Redis server password. This is an optional variable. Default value is an empty string.', 'introduction' => '0.7', 'default' => '', 'required' => false, diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index f3a4022d03..1ec5dad29b 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -126,7 +126,7 @@ App::post('/v1/account') throw new Exception('Account already exists', 409); } - Authorization::enable(); + Authorization::reset(); Authorization::unsetRole('role:'.Auth::USER_ROLE_GUEST); Authorization::setRole('user:'.$user->getId()); @@ -208,10 +208,11 @@ App::post('/v1/account/sessions') $secret = Auth::tokenGenerator(); $session = new Document(array_merge( [ - '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$collection' => Database::SYSTEM_COLLECTION_SESSIONS, '$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]], 'userId' => $profile->getId(), - 'type' => Auth::TOKEN_TYPE_LOGIN, + 'provider' => Auth::SESSION_PROVIDER_EMAIL, + 'providerUid' => $email, 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak 'expire' => $expiry, 'userAgent' => $request->getUserAgent('UNKNOWN'), @@ -228,7 +229,7 @@ App::post('/v1/account/sessions') throw new Exception('Failed saving session to DB', 500); } - $profile->setAttribute('tokens', $session, Document::SET_TYPE_APPEND); + $profile->setAttribute('sessions', $session, Document::SET_TYPE_APPEND); $profile = $projectDB->updateDocument($profile->getArrayCopy()); @@ -254,9 +255,11 @@ App::post('/v1/account/sessions') ->setStatusCode(Response::STATUS_CODE_CREATED) ; + $countries = $locale->getText('countries'); + $session ->setAttribute('current', true) - ->setAttribute('countryName', (isset($countries[$session->getAttribute('countryCode')])) ? $countries[$session->getAttribute('countryCode')] : $locale->getText('locale.country.unknown')) + ->setAttribute('countryName', (isset($countries[strtoupper($session->getAttribute('countryCode'))])) ? $countries[strtoupper($session->getAttribute('countryCode'))] : $locale->getText('locale.country.unknown')) ; $response->dynamic($session, Response::MODEL_SESSION); @@ -271,8 +274,8 @@ App::get('/v1/account/sessions/oauth2/:provider') ->label('sdk.namespace', 'account') ->label('sdk.method', 'createOAuth2Session') ->label('sdk.description', '/docs/references/account/create-session-oauth2.md') - ->label('sdk.response.code', 301) - ->label('sdk.response.type', 'text/html') + ->label('sdk.response.code', Response::STATUS_CODE_MOVED_PERMANENTLY) + ->label('sdk.response.type', Response::CONTENT_TYPE_HTML) ->label('sdk.methodType', 'webAuth') ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') @@ -457,7 +460,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') throw new Exception('Missing ID from OAuth2 provider', 400); } - $current = Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret); + $current = Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret); if ($current) { $projectDB->deleteDocument($current); //throw new Exception('User already logged in', 401); @@ -467,7 +470,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'limit' => 1, 'filters' => [ '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'oauth2'.\ucfirst($provider).'='.$oauth2ID, + 'sessions.provider='.$provider, + 'sessions.providerUid='.$oauth2ID ], ]) : $user; @@ -519,7 +523,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') throw new Exception('Account already exists', 409); } - Authorization::enable(); + Authorization::reset(); if (false === $user) { throw new Exception('Failed saving user to DB', 500); @@ -538,10 +542,12 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $secret = Auth::tokenGenerator(); $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; $session = new Document(array_merge([ - '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$collection' => Database::SYSTEM_COLLECTION_SESSIONS, '$permissions' => ['read' => ['user:'.$user['$id']], 'write' => ['user:'.$user['$id']]], 'userId' => $user->getId(), - 'type' => Auth::TOKEN_TYPE_LOGIN, + 'provider' => $provider, + 'providerUid' => $oauth2ID, + 'providerToken' => $accessToken, 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak 'expire' => $expiry, 'userAgent' => $request->getUserAgent('UNKNOWN'), @@ -549,11 +555,18 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', ], $detector->getOS(), $detector->getClient(), $detector->getDevice())); + $isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); + + if ($isAnonymousUser) { + $user + ->setAttribute('name', $oauth2->getUserName($accessToken)) + ->setAttribute('email', $oauth2->getUserEmail($accessToken)) + ; + } + $user - ->setAttribute('oauth2'.\ucfirst($provider), $oauth2ID) - ->setAttribute('oauth2'.\ucfirst($provider).'AccessToken', $accessToken) ->setAttribute('status', Auth::USER_STATUS_ACTIVATED) - ->setAttribute('tokens', $session, Document::SET_TYPE_APPEND) + ->setAttribute('sessions', $session, Document::SET_TYPE_APPEND) ; Authorization::setRole('user:'.$user->getId()); @@ -598,6 +611,130 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ; }); +App::post('/v1/account/sessions/anonymous') + ->desc('Create Anonymous Session') + ->groups(['api', 'account']) + ->label('event', 'account.sessions.create') + ->label('scope', 'public') + ->label('sdk.platform', [APP_PLATFORM_CLIENT]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'createAnonymousSession') + ->label('sdk.description', '/docs/references/account/create-session-anonymous.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_SESSION) + ->label('abuse-limit', 50) + ->label('abuse-key', 'ip:{ip}') + ->inject('request') + ->inject('response') + ->inject('locale') + ->inject('user') + ->inject('project') + ->inject('projectDB') + ->inject('geodb') + ->inject('audits') + ->action(function ($request, $response, $locale, $user, $project, $projectDB, $geodb, $audits) { + /** @var Utopia\Swoole\Request $request */ + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Locale\Locale $locale */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var MaxMind\Db\Reader $geodb */ + /** @var Appwrite\Event\Event $audits */ + + $protocol = $request->getProtocol(); + + if ($user->getId() || 'console' === $project->getId()) { + throw new Exception('Failed to create anonymous user.', 401); + } + + Authorization::disable(); + try { + $user = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => [ + 'read' => ['*'], + 'write' => ['user:{self}'] + ], + 'email' => null, + 'emailVerification' => false, + 'status' => Auth::USER_STATUS_UNACTIVATED, + 'password' => null, + 'passwordUpdate' => \time(), + 'registration' => \time(), + 'reset' => false, + 'name' => null + ]); + } catch (Exception $th) { + throw new Exception('Failed saving user to DB', 500); + } + + Authorization::reset(); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + // Create session token + + $detector = new Detector($request->getUserAgent('UNKNOWN')); + $record = $geodb->get($request->getIP()); + $secret = Auth::tokenGenerator(); + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $session = new Document(array_merge( + [ + '$collection' => Database::SYSTEM_COLLECTION_SESSIONS, + '$permissions' => ['read' => ['user:' . $user['$id']], 'write' => ['user:' . $user['$id']]], + 'userId' => $user->getId(), + 'provider' => Auth::SESSION_PROVIDER_ANONYMOUS, + 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak + 'expire' => $expiry, + 'userAgent' => $request->getUserAgent('UNKNOWN'), + 'ip' => $request->getIP(), + 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', + ], + $detector->getOS(), + $detector->getClient(), + $detector->getDevice() + )); + + $user->setAttribute('sessions', $session, Document::SET_TYPE_APPEND); + + Authorization::setRole('user:'.$user->getId()); + + $user = $projectDB->updateDocument($user->getArrayCopy()); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $audits + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.sessions.create') + ->setParam('resource', 'users/'.$user->getId()) + ; + + if (!Config::getParam('domainVerification')) { + $response + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) + ; + } + + $response + ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->setStatusCode(Response::STATUS_CODE_CREATED) + ; + + $session + ->setAttribute('current', true) + ->setAttribute('countryName', (isset($countries[$session->getAttribute('countryCode')])) ? $countries[$session->getAttribute('countryCode')] : $locale->getText('locale.country.unknown')) + ; + + $response->dynamic($session, Response::MODEL_SESSION); + }); + App::post('/v1/account/jwt') ->desc('Create Account JWT') ->groups(['api', 'account', 'auth']) @@ -616,16 +753,18 @@ App::post('/v1/account/jwt') /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $user */ - $tokens = $user->getAttribute('tokens', []); - $session = new Document(); + $sessions = $user->getAttribute('sessions', []); + $current = new Document(); - foreach ($tokens as $token) { /** @var Appwrite\Database\Document $token */ - if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too - $session = $token; + foreach ($sessions as $session) { + /** @var Appwrite\Database\Document $session */ + + if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + $current = $session; } } - if($session->isEmpty()) { + if($current->isEmpty()) { throw new Exception('No valid session found', 401); } @@ -639,7 +778,7 @@ App::post('/v1/account/jwt') // 'scopes' => ['user'], // 'iss' => 'http://api.mysite.com', 'userId' => $user->getId(), - 'sessionId' => $session->getId(), + 'sessionId' => $current->getId(), ])]), Response::MODEL_JWT); }); @@ -704,22 +843,19 @@ App::get('/v1/account/sessions') /** @var Appwrite\Database\Document $user */ /** @var Utopia\Locale\Locale $locale */ - $tokens = $user->getAttribute('tokens', []); - $sessions = []; + $sessions = $user->getAttribute('sessions', []); $countries = $locale->getText('countries'); - $current = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_LOGIN, Auth::$secret); + $current = Auth::sessionVerify($sessions, Auth::$secret); - foreach ($tokens as $token) { /* @var $token Document */ - if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) { - continue; - } + foreach ($sessions as $key => $session) { + /** @var Document $session */ - $token->setAttribute('countryName', (isset($countries[$token->getAttribute('contryCode')])) - ? $countries[$token->getAttribute('contryCode')] + $session->setAttribute('countryName', (isset($countries[strtoupper($session->getAttribute('countryCode'))])) + ? $countries[strtoupper($session->getAttribute('countryCode'))] : $locale->getText('locale.country.unknown')); - $token->setAttribute('current', ($current == $token->getId()) ? true : false); + $session->setAttribute('current', ($current == $session->getId()) ? true : false); - $sessions[] = $token; + $sessions[$key] = $session; } $response->dynamic(new Document([ @@ -913,7 +1049,12 @@ App::patch('/v1/account/email') /** @var Appwrite\Database\Database $projectDB */ /** @var Appwrite\Event\Event $audits */ - if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password + $isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); // Check if request is from an anonymous account for converting + + if ( + !$isAnonymousUser && + !Auth::passwordVerify($password, $user->getAttribute('password')) + ) { // Double check user password throw new Exception('Invalid credentials', 401); } @@ -931,10 +1072,14 @@ App::patch('/v1/account/email') // TODO after this user needs to confirm mail again - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'email' => $email, - 'emailVerification' => false, - ])); + $user = $projectDB->updateDocument(\array_merge( + $user->getArrayCopy(), + ($isAnonymousUser ? [ 'password' => Auth::passwordHash($password) ] : []), + [ + 'email' => $email, + 'emailVerification' => false, + ] + )); if (false === $user) { throw new Exception('Failed saving user to DB', 500); @@ -1038,7 +1183,7 @@ App::delete('/v1/account') ; $events - ->setParam('payload', $response->output($user, Response::MODEL_USER)) + ->setParam('eventData', $response->output($user, Response::MODEL_USER)) ; if (!Config::getParam('domainVerification')) { @@ -1083,14 +1228,16 @@ App::delete('/v1/account/sessions/:sessionId') $protocol = $request->getProtocol(); $sessionId = ($sessionId === 'current') - ? Auth::tokenVerify($user->getAttribute('tokens'), Auth::TOKEN_TYPE_LOGIN, Auth::$secret) + ? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret) : $sessionId; - $tokens = $user->getAttribute('tokens', []); + $sessions = $user->getAttribute('sessions', []); - foreach ($tokens as $token) { /* @var $token Document */ - if (($sessionId == $token->getId()) && Auth::TOKEN_TYPE_LOGIN == $token->getAttribute('type')) { - if (!$projectDB->deleteDocument($token->getId())) { + foreach ($sessions as $session) { + /** @var Document $session */ + + if (($sessionId == $session->getId())) { + if (!$projectDB->deleteDocument($session->getId())) { throw new Exception('Failed to remove token from DB', 500); } @@ -1106,10 +1253,10 @@ App::delete('/v1/account/sessions/:sessionId') ; } - $token->setAttribute('current', false); + $session->setAttribute('current', false); - if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too - $token->setAttribute('current', true); + if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + $session->setAttribute('current', true); $response ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) @@ -1118,7 +1265,7 @@ App::delete('/v1/account/sessions/:sessionId') } $events - ->setParam('payload', $response->output($token, Response::MODEL_SESSION)) + ->setParam('eventData', $response->output($session, Response::MODEL_SESSION)) ; return $response->noContent(); @@ -1155,10 +1302,12 @@ App::delete('/v1/account/sessions') /** @var Appwrite\Event\Event $events */ $protocol = $request->getProtocol(); - $tokens = $user->getAttribute('tokens', []); + $sessions = $user->getAttribute('sessions', []); - foreach ($tokens as $token) { /* @var $token Document */ - if (!$projectDB->deleteDocument($token->getId())) { + foreach ($sessions as $session) { + /** @var Document $session */ + + if (!$projectDB->deleteDocument($session->getId())) { throw new Exception('Failed to remove token from DB', 500); } @@ -1174,10 +1323,10 @@ App::delete('/v1/account/sessions') ; } - $token->setAttribute('current', false); + $session->setAttribute('current', false); - if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too - $token->setAttribute('current', true); + if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + $session->setAttribute('current', true); $response ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) @@ -1186,9 +1335,9 @@ App::delete('/v1/account/sessions') } $events - ->setParam('payload', $response->output(new Document([ - 'sum' => count($tokens), - 'sessions' => $tokens + ->setParam('eventData', $response->output(new Document([ + 'sum' => count($sessions), + 'sessions' => $sessions ]), Response::MODEL_SESSION_LIST)) ; @@ -1311,7 +1460,7 @@ App::post('/v1/account/recovery') ; $events - ->setParam('payload', + ->setParam('eventData', $response->output($recovery->setAttribute('secret', $secret), Response::MODEL_TOKEN )) @@ -1514,7 +1663,7 @@ App::post('/v1/account/verification') ; $events - ->setParam('payload', + ->setParam('eventData', $response->output($verification->setAttribute('secret', $verificationSecret), Response::MODEL_TOKEN )) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 77de5953ad..e86812e7e3 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -169,8 +169,8 @@ App::put('/v1/database/collections/:collectionId') ->label('sdk.response.model', Response::MODEL_COLLECTION) ->param('collectionId', '', new UID(), 'Collection unique ID.') ->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.') - ->param('read', [], new ArrayList(new Text(64)), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions(/docs/permissions) and get a full list of available permissions.') - ->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') + ->param('read', null, new ArrayList(new Text(64)), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) + ->param('write', null, new ArrayList(new Text(64)), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->param('rules', [], function ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.', true, ['projectDB']) ->inject('response') ->inject('projectDB') @@ -187,6 +187,8 @@ App::put('/v1/database/collections/:collectionId') } $parsedRules = []; + $read = (is_null($read)) ? ($collection->getPermissions()['read'] ?? []) : $read; // By default inherit read permissions + $write = (is_null($write)) ? ($collection->getPermissions()['write'] ?? []) : $write; // By default inherit write permissions foreach ($rules as &$rule) { $parsedRules[] = \array_merge([ @@ -269,7 +271,7 @@ App::delete('/v1/database/collections/:collectionId') ; $events - ->setParam('payload', $response->output($collection, Response::MODEL_COLLECTION)) + ->setParam('eventData', $response->output($collection, Response::MODEL_COLLECTION)) ; $audits @@ -295,17 +297,19 @@ App::post('/v1/database/collections/:collectionId/documents') ->label('sdk.response.model', Response::MODEL_ANY) ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') ->param('data', [], new JSON(), 'Document data as JSON object.') - ->param('read', [], new ArrayList(new Text(64)), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - ->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') + ->param('read', null, new ArrayList(new Text(64)), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) + ->param('write', null, new ArrayList(new Text(64)), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->param('parentDocument', '', new UID(), 'Parent document unique ID. Use when you want your new document to be a child of a parent document.', true) ->param('parentProperty', '', new Key(), 'Parent document property name. Use when you want your new document to be a child of a parent document.', true) ->param('parentPropertyType', Document::SET_TYPE_ASSIGN, new WhiteList([Document::SET_TYPE_ASSIGN, Document::SET_TYPE_APPEND, Document::SET_TYPE_PREPEND], true), 'Parent document property connection type. You can set this value to **assign**, **append** or **prepend**, default value is assign. Use when you want your new document to be a child of a parent document.', true) ->inject('response') ->inject('projectDB') + ->inject('user') ->inject('audits') - ->action(function ($collectionId, $data, $read, $write, $parentDocument, $parentProperty, $parentPropertyType, $response, $projectDB, $audits) { + ->action(function ($collectionId, $data, $read, $write, $parentDocument, $parentProperty, $parentPropertyType, $response, $projectDB, $user, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Event\Event $audits */ $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -326,8 +330,8 @@ App::post('/v1/database/collections/:collectionId/documents') $data['$collection'] = $collectionId; // Adding this param to make API easier for developers $data['$permissions'] = [ - 'read' => $read, - 'write' => $write, + 'read' => (is_null($read) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $read ?? [], // By default set read permissions for user + 'write' => (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? [], // By default set write permissions for user ]; // Read parent document + validate not 404 + validate read / write permission like patch method @@ -508,8 +512,8 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') ->param('documentId', null, new UID(), 'Document unique ID.') ->param('data', [], new JSON(), 'Document data as JSON object.') - ->param('read', [], new ArrayList(new Text(64)), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - ->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') + ->param('read', null, new ArrayList(new Text(64)), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) + ->param('write', null, new ArrayList(new Text(64)), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->inject('response') ->inject('projectDB') ->inject('audits') @@ -522,7 +526,7 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') $document = $projectDB->getDocument($documentId, false); $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array - + if (!\is_array($data)) { throw new Exception('Data param should be a valid JSON object', 400); } @@ -535,20 +539,12 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') throw new Exception('No document found', 404); } - //TODO check merge read write permissions - - if (!empty($read)) { // Overwrite permissions only when passed - $data['$permissions']['read'] = $read; - } - - if (!empty($write)) { // Overwrite permissions only when passed - $data['$permissions']['write'] = $write; - } - $data = \array_merge($document->getArrayCopy(), $data); $data['$collection'] = $collection->getId(); // Make sure user don't switch collectionID $data['$id'] = $document->getId(); // Make sure user don't switch document unique ID + $data['$permissions']['read'] = (is_null($read)) ? ($document->getPermissions()['read'] ?? []) : $read; // By default inherit read permissions + $data['$permissions']['write'] = (is_null($write)) ? ($document->getPermissions()['write'] ?? []) : $write; // By default inherit write permissions if (empty($data)) { throw new Exception('Missing payload', 400); @@ -618,7 +614,7 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId') } $events - ->setParam('payload', $response->output($document, Response::MODEL_ANY)) + ->setParam('eventData', $response->output($document, Response::MODEL_ANY)) ; $audits diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 7a6b11bcda..c2664f9dc3 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -1,5 +1,7 @@ groups(['api', 'functions']) ->desc('Create Function') ->label('scope', 'functions.write') + ->label('event', 'functions.create') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'create') @@ -265,6 +268,7 @@ App::put('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Update Function') ->label('scope', 'functions.write') + ->label('event', 'functions.update') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'update') @@ -330,6 +334,7 @@ App::patch('/v1/functions/:functionId/tag') ->groups(['api', 'functions']) ->desc('Update Function Tag') ->label('scope', 'functions.write') + ->label('event', 'functions.tags.update') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'updateTag') @@ -387,6 +392,7 @@ App::delete('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Delete Function') ->label('scope', 'functions.write') + ->label('event', 'functions.delete') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'delete') @@ -424,6 +430,7 @@ App::post('/v1/functions/:functionId/tags') ->groups(['api', 'functions']) ->desc('Create Tag') ->label('scope', 'functions.write') + ->label('event', 'functions.tags.create') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'createTag') @@ -435,7 +442,7 @@ App::post('/v1/functions/:functionId/tags') ->label('sdk.response.model', Response::MODEL_TAG) ->param('functionId', '', new UID(), 'Function unique ID.') ->param('command', '', new Text('1028'), 'Code execution command.') - ->param('code', null, new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', false) + ->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', false) ->inject('request') ->inject('response') ->inject('projectDB') @@ -601,6 +608,7 @@ App::delete('/v1/functions/:functionId/tags/:tagId') ->groups(['api', 'functions']) ->desc('Delete Tag') ->label('scope', 'functions.write') + ->label('event', 'functions.tags.delete') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'deleteTag') @@ -662,6 +670,7 @@ App::post('/v1/functions/:functionId/executions') ->groups(['api', 'functions']) ->desc('Create Execution') ->label('scope', 'execution.write') + ->label('event', 'functions.executions.create') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'createExecution') @@ -672,14 +681,17 @@ App::post('/v1/functions/:functionId/executions') ->label('abuse-limit', 60) ->label('abuse-time', 60) ->param('functionId', '', new UID(), 'Function unique ID.') + ->param('data', '', new Text(8192), 'String of custom data to send to function.', true) // ->param('async', 1, new Range(0, 1), 'Execute code asynchronously. Pass 1 for true, 0 for false. Default value is 1.', true) ->inject('response') ->inject('project') ->inject('projectDB') - ->action(function ($functionId, /*$async,*/ $response, $project, $projectDB) { + ->inject('user') + ->action(function ($functionId, $data, /*$async,*/ $response, $project, $projectDB, $user) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $project */ /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Database\Document $user */ Authorization::disable(); @@ -712,7 +724,7 @@ App::post('/v1/functions/:functionId/executions') $execution = $projectDB->createDocument([ '$collection' => Database::SYSTEM_COLLECTION_EXECUTIONS, '$permissions' => [ - 'read' => $function->getPermissions()['execute'] ?? [], + 'read' => (!empty($user->getId())) ? ['user:' . $user->getId()] : [], 'write' => [], ], 'dateCreated' => time(), @@ -730,12 +742,36 @@ App::post('/v1/functions/:functionId/executions') if (false === $execution) { throw new Exception('Failed saving execution to DB', 500); } - + + $jwt = ''; // initialize + if (!empty($user->getId())) { // If userId exists, generate a JWT for function + + $tokens = $user->getAttribute('tokens', []); + $session = new Document(); + + foreach ($tokens as $token) { /** @var Appwrite\Database\Document $token */ + if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + $session = $token; + } + } + + if(!$session->isEmpty()) { + $jwtObj = new JWT(App::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. + $jwt = $jwtObj->encode([ + 'userId' => $user->getId(), + 'sessionId' => $session->getId(), + ]); + } + } + Resque::enqueue('v1-functions', 'FunctionsV1', [ 'projectId' => $project->getId(), 'functionId' => $function->getId(), 'executionId' => $execution->getId(), 'trigger' => 'http', + 'data' => $data, + 'userId' => $user->getId(), + 'jwt' => $jwt, ]); $response diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 696247f3da..a4201d54d9 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -254,7 +254,10 @@ App::get('/v1/health/anti-virus') /** @var Appwrite\Utopia\Response $response */ if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'disabled') { // Check if scans are enabled - throw new Exception('Anitvirus is disabled'); + return $response->json([ + 'status' => 'disabled', + 'version' => '', + ]); } $antiVirus = new Network(App::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'), diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index d4500e95aa..50570cb504 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -38,17 +38,19 @@ App::post('/v1/storage/files') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_FILE) ->param('file', [], new File(), 'Binary file.', false) - ->param('read', [], new ArrayList(new Text(64)), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - ->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') + ->param('read', null, new ArrayList(new Text(64)), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) + ->param('write', null, new ArrayList(new Text(64)), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->inject('request') ->inject('response') ->inject('projectDB') + ->inject('user') ->inject('audits') ->inject('usage') - ->action(function ($file, $read, $write, $request, $response, $projectDB, $audits, $usage) { + ->action(function ($file, $read, $write, $request, $response, $projectDB, $user, $audits, $usage) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Event\Event $audits */ /** @var Appwrite\Event\Event $usage */ @@ -122,8 +124,8 @@ App::post('/v1/storage/files') $file = $projectDB->createDocument([ '$collection' => Database::SYSTEM_COLLECTION_FILES, '$permissions' => [ - 'read' => $read, - 'write' => $write, + 'read' => (is_null($read) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $read ?? [], // By default set read permissions for user + 'write' => (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? [], // By default set write permissions for user ], 'dateCreated' => \time(), 'folderId' => '', @@ -579,7 +581,7 @@ App::delete('/v1/storage/files/:fileId') ; $events - ->setParam('payload', $response->output($file, Response::MODEL_FILE)) + ->setParam('eventData', $response->output($file, Response::MODEL_FILE)) ; $response->noContent(); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index e392dbb64b..fba31271c3 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -243,7 +243,7 @@ App::delete('/v1/teams/:teamId') } $events - ->setParam('payload', $response->output($team, Response::MODEL_TEAM)) + ->setParam('eventData', $response->output($team, Response::MODEL_TEAM)) ; $response->noContent(); @@ -344,6 +344,7 @@ App::post('/v1/teams/:teamId/memberships') 'registration' => \time(), 'reset' => false, 'name' => $name, + 'sessions' => [], 'tokens' => [], ], ['email' => $email]); } catch (Duplicate $th) { @@ -612,10 +613,11 @@ App::patch('/v1/teams/:teamId/memberships/:inviteId/status') $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; $secret = Auth::tokenGenerator(); $session = new Document(array_merge([ - '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$collection' => Database::SYSTEM_COLLECTION_SESSIONS, '$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]], 'userId' => $user->getId(), - 'type' => Auth::TOKEN_TYPE_LOGIN, + 'provider' => Auth::SESSION_PROVIDER_EMAIL, + 'providerUid' => $user->getAttribute('email'), 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak 'expire' => $expiry, 'userAgent' => $request->getUserAgent('UNKNOWN'), @@ -623,7 +625,7 @@ App::patch('/v1/teams/:teamId/memberships/:inviteId/status') 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', ], $detector->getOS(), $detector->getClient(), $detector->getDevice())); - $user->setAttribute('tokens', $session, Document::SET_TYPE_APPEND); + $user->setAttribute('sessions', $session, Document::SET_TYPE_APPEND); Authorization::setRole('user:'.$userId); @@ -728,7 +730,7 @@ App::delete('/v1/teams/:teamId/memberships/:inviteId') ; $events - ->setParam('payload', $response->output($membership, Response::MODEL_MEMBERSHIP)) + ->setParam('eventData', $response->output($membership, Response::MODEL_MEMBERSHIP)) ; $response->noContent(); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index efb0041cee..3f6a82d9c0 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -196,21 +196,18 @@ App::get('/v1/users/:userId/sessions') throw new Exception('User not found', 404); } - $tokens = $user->getAttribute('tokens', []); - $sessions = []; + $sessions = $user->getAttribute('sessions', []); $countries = $locale->getText('countries'); - foreach ($tokens as $token) { /* @var $token Document */ - if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) { - continue; - } + foreach ($sessions as $key => $session) { + /** @var Document $session */ - $token->setAttribute('countryName', (isset($countries[$token->getAttribute('contryCode')])) - ? $countries[$token->getAttribute('contryCode')] + $session->setAttribute('countryName', (isset($countries[strtoupper($session->getAttribute('countryCode'))])) + ? $countries[strtoupper($session->getAttribute('countryCode'))] : $locale->getText('locale.country.unknown')); - $token->setAttribute('current', false); + $session->setAttribute('current', false); - $sessions[] = $token; + $sessions[$key] = $session; } $response->dynamic(new Document([ @@ -373,6 +370,7 @@ App::patch('/v1/users/:userId/status') App::patch('/v1/users/:userId/prefs') ->desc('Update User Preferences') ->groups(['api', 'users']) + ->label('event', 'users.update.prefs') ->label('scope', 'users.write') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -433,16 +431,18 @@ App::delete('/v1/users/:userId/sessions/:sessionId') throw new Exception('User not found', 404); } - $tokens = $user->getAttribute('tokens', []); + $sessions = $user->getAttribute('sessions', []); - foreach ($tokens as $token) { /* @var $token Document */ - if ($sessionId == $token->getId()) { - if (!$projectDB->deleteDocument($token->getId())) { + foreach ($sessions as $session) { + /** @var Document $session */ + + if ($sessionId == $session->getId()) { + if (!$projectDB->deleteDocument($session->getId())) { throw new Exception('Failed to remove token from DB', 500); } $events - ->setParam('payload', $response->output($user, Response::MODEL_USER)) + ->setParam('eventData', $response->output($user, Response::MODEL_USER)) ; } } @@ -477,16 +477,18 @@ App::delete('/v1/users/:userId/sessions') throw new Exception('User not found', 404); } - $tokens = $user->getAttribute('tokens', []); + $sessions = $user->getAttribute('sessions', []); - foreach ($tokens as $token) { /* @var $token Document */ - if (!$projectDB->deleteDocument($token->getId())) { + foreach ($sessions as $session) { + /** @var Document $session */ + + if (!$projectDB->deleteDocument($session->getId())) { throw new Exception('Failed to remove token from DB', 500); } } $events - ->setParam('payload', $response->output($user, Response::MODEL_USER)) + ->setParam('eventData', $response->output($user, Response::MODEL_USER)) ; // TODO : Response filter implementation @@ -546,7 +548,7 @@ App::delete('/v1/users/:userId') ; $events - ->setParam('payload', $response->output($user, Response::MODEL_USER)) + ->setParam('eventData', $response->output($user, Response::MODEL_USER)) ; // TODO : Response filter implementation diff --git a/app/controllers/general.php b/app/controllers/general.php index cfe8474755..231a93445e 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -117,7 +117,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo ->addHeader('Server', 'Appwrite') ->addHeader('X-Content-Type-Options', 'nosniff') ->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE') - ->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-SDK-Version, Cache-Control, Expires, Pragma') + ->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, Cache-Control, Expires, Pragma') ->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies') ->addHeader('Access-Control-Allow-Origin', $refDomain) ->addHeader('Access-Control-Allow-Credentials', 'true') @@ -237,7 +237,7 @@ App::options(function ($request, $response) { $response ->addHeader('Server', 'Appwrite') ->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE') - ->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-SDK-Version, Cache-Control, Expires, Pragma, X-Fallback-Cookies') + ->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, Cache-Control, Expires, Pragma, X-Fallback-Cookies') ->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies') ->addHeader('Access-Control-Allow-Origin', $origin) ->addHeader('Access-Control-Allow-Credentials', 'true') @@ -256,6 +256,8 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project) { $template = ($route) ? $route->getLabel('error', null) : null; if (php_sapi_name() === 'cli') { + Console::error('[Error] Timestamp: '.date('c', time())); + if($route) { Console::error('[Error] Method: '.$route->getMethod()); Console::error('[Error] URL: '.$route->getURL()); diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 4c9f3faf9f..7e65e2d936 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -2,8 +2,9 @@ global $utopia, $request, $response; +use Appwrite\Database\Document; +use Appwrite\Utopia\Response; use Utopia\App; -use Utopia\Response; use Utopia\Validator\Numeric; use Utopia\Validator\Text; use Utopia\Validator\ArrayList; @@ -11,13 +12,16 @@ use Utopia\Validator\Host; use Utopia\Storage\Validator\File; App::get('/v1/mock/tests/foo') - ->desc('Mock a get request for SDK tests') + ->desc('Get Foo') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'foo') ->label('sdk.method', 'get') - ->label('sdk.description', 'Mock a get request for SDK tests') + ->label('sdk.description', 'Mock a get request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->param('x', '', new Text(100), 'Sample string param') ->param('y', '', new Numeric(), 'Sample numeric param') @@ -26,13 +30,16 @@ App::get('/v1/mock/tests/foo') }); App::post('/v1/mock/tests/foo') - ->desc('Mock a post request for SDK tests') + ->desc('Post Foo') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'foo') ->label('sdk.method', 'post') - ->label('sdk.description', 'Mock a post request for SDK tests') + ->label('sdk.description', 'Mock a post request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->param('x', '', new Text(100), 'Sample string param') ->param('y', '', new Numeric(), 'Sample numeric param') @@ -41,13 +48,16 @@ App::post('/v1/mock/tests/foo') }); App::patch('/v1/mock/tests/foo') - ->desc('Mock a patch request for SDK tests') + ->desc('Patch Foo') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'foo') ->label('sdk.method', 'patch') - ->label('sdk.description', 'Mock a get request for SDK tests') + ->label('sdk.description', 'Mock a patch request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->param('x', '', new Text(100), 'Sample string param') ->param('y', '', new Numeric(), 'Sample numeric param') @@ -56,13 +66,16 @@ App::patch('/v1/mock/tests/foo') }); App::put('/v1/mock/tests/foo') - ->desc('Mock a put request for SDK tests') + ->desc('Put Foo') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'foo') ->label('sdk.method', 'put') - ->label('sdk.description', 'Mock a put request for SDK tests') + ->label('sdk.description', 'Mock a put request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->param('x', '', new Text(100), 'Sample string param') ->param('y', '', new Numeric(), 'Sample numeric param') @@ -71,13 +84,16 @@ App::put('/v1/mock/tests/foo') }); App::delete('/v1/mock/tests/foo') - ->desc('Mock a delete request for SDK tests') + ->desc('Delete Foo') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'foo') ->label('sdk.method', 'delete') - ->label('sdk.description', 'Mock a delete request for SDK tests') + ->label('sdk.description', 'Mock a delete request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->param('x', '', new Text(100), 'Sample string param') ->param('y', '', new Numeric(), 'Sample numeric param') @@ -86,13 +102,16 @@ App::delete('/v1/mock/tests/foo') }); App::get('/v1/mock/tests/bar') - ->desc('Mock a get request for SDK tests') + ->desc('Get Bar') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'bar') ->label('sdk.method', 'get') - ->label('sdk.description', 'Mock a get request for SDK tests') + ->label('sdk.description', 'Mock a get request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->param('x', '', new Text(100), 'Sample string param') ->param('y', '', new Numeric(), 'Sample numeric param') @@ -101,13 +120,16 @@ App::get('/v1/mock/tests/bar') }); App::post('/v1/mock/tests/bar') - ->desc('Mock a post request for SDK tests') + ->desc('Post Bar') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'bar') ->label('sdk.method', 'post') - ->label('sdk.description', 'Mock a post request for SDK tests') + ->label('sdk.description', 'Mock a post request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->param('x', '', new Text(100), 'Sample string param') ->param('y', '', new Numeric(), 'Sample numeric param') @@ -116,13 +138,16 @@ App::post('/v1/mock/tests/bar') }); App::patch('/v1/mock/tests/bar') - ->desc('Mock a patch request for SDK tests') + ->desc('Patch Bar') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'bar') ->label('sdk.method', 'patch') - ->label('sdk.description', 'Mock a get request for SDK tests') + ->label('sdk.description', 'Mock a patch request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->param('x', '', new Text(100), 'Sample string param') ->param('y', '', new Numeric(), 'Sample numeric param') @@ -131,13 +156,16 @@ App::patch('/v1/mock/tests/bar') }); App::put('/v1/mock/tests/bar') - ->desc('Mock a put request for SDK tests') + ->desc('Put Bar') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'bar') ->label('sdk.method', 'put') - ->label('sdk.description', 'Mock a put request for SDK tests') + ->label('sdk.description', 'Mock a put request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->param('x', '', new Text(100), 'Sample string param') ->param('y', '', new Numeric(), 'Sample numeric param') @@ -146,13 +174,16 @@ App::put('/v1/mock/tests/bar') }); App::delete('/v1/mock/tests/bar') - ->desc('Mock a delete request for SDK tests') + ->desc('Delete Bar') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'bar') ->label('sdk.method', 'delete') - ->label('sdk.description', 'Mock a delete request for SDK tests') + ->label('sdk.description', 'Mock a delete request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->param('x', '', new Text(100), 'Sample string param') ->param('y', '', new Numeric(), 'Sample numeric param') @@ -161,14 +192,17 @@ App::delete('/v1/mock/tests/bar') }); App::post('/v1/mock/tests/general/upload') - ->desc('Mock a post request for SDK tests') + ->desc('Upload File') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'general') ->label('sdk.method', 'upload') - ->label('sdk.description', 'Mock a delete request for SDK tests') + ->label('sdk.description', 'Mock a file upload request.') ->label('sdk.request.type', 'multipart/form-data') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->param('x', '', new Text(100), 'Sample string param') ->param('y', '', new Numeric(), 'Sample numeric param') @@ -203,13 +237,15 @@ App::post('/v1/mock/tests/general/upload') }); App::get('/v1/mock/tests/general/redirect') - ->desc('Mock a post request for SDK tests') + ->desc('Redirect') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'general') ->label('sdk.method', 'redirect') - ->label('sdk.description', 'Mock a redirect request for SDK tests') + ->label('sdk.description', 'Mock a redirect request.') + ->label('sdk.response.code', Response::STATUS_CODE_MOVED_PERMANENTLY) + ->label('sdk.response.type', Response::CONTENT_TYPE_HTML) ->label('sdk.mock', true) ->inject('response') ->action(function ($response) { @@ -219,25 +255,31 @@ App::get('/v1/mock/tests/general/redirect') }); App::get('/v1/mock/tests/general/redirect/done') - ->desc('Mock a post request for SDK tests') + ->desc('Redirected') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'general') ->label('sdk.method', 'redirected') - ->label('sdk.description', 'Mock a redirected request for SDK tests') + ->label('sdk.description', 'Mock a redirected request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->action(function () { }); App::get('/v1/mock/tests/general/set-cookie') - ->desc('Mock a cookie request for SDK tests') + ->desc('Set Cookie') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'general') ->label('sdk.method', 'setCookie') - ->label('sdk.description', 'Mock a set cookie request for SDK tests') + ->label('sdk.description', 'Mock a set cookie request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->inject('response') ->action(function ($response) { @@ -247,13 +289,16 @@ App::get('/v1/mock/tests/general/set-cookie') }); App::get('/v1/mock/tests/general/get-cookie') - ->desc('Mock a cookie request for SDK tests') + ->desc('Get Cookie') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'general') ->label('sdk.method', 'getCookie') - ->label('sdk.description', 'Mock a get cookie request for SDK tests') + ->label('sdk.description', 'Mock a cookie response.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) ->label('sdk.mock', true) ->inject('request') ->action(function ($request) { @@ -265,13 +310,15 @@ App::get('/v1/mock/tests/general/get-cookie') }); App::get('/v1/mock/tests/general/empty') - ->desc('Mock a post request for SDK tests') + ->desc('Empty Response') ->groups(['mock']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'general') ->label('sdk.method', 'empty') - ->label('sdk.description', 'Mock a redirected request for SDK tests') + ->label('sdk.description', 'Mock a an empty response.') + ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) + ->label('sdk.response.model', Response::MODEL_NONE) ->label('sdk.mock', true) ->inject('response') ->action(function ($response) { @@ -280,8 +327,40 @@ App::get('/v1/mock/tests/general/empty') $response->noContent(); }); +App::get('/v1/mock/tests/general/400-error') + ->desc('400 Error') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'error400') + ->label('sdk.description', 'Mock a an 400 failed request.') + ->label('sdk.response.code', Response::STATUS_CODE_BAD_REQUEST) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_ERROR) + ->label('sdk.mock', true) + ->action(function () { + throw new Exception('Mock 400 error', 400); + }); + +App::get('/v1/mock/tests/general/500-error') + ->desc('500 Error') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'error500') + ->label('sdk.description', 'Mock a an 500 failed request.') + ->label('sdk.response.code', Response::STATUS_CODE_INTERNAL_SERVER_ERROR) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_ERROR) + ->label('sdk.mock', true) + ->action(function () { + throw new Exception('Mock 500 error', 500); + }); + App::get('/v1/mock/tests/general/oauth2') - ->desc('Mock an OAuth2 login route') + ->desc('OAuth Login') ->groups(['mock']) ->label('scope', 'public') ->label('docs', false) @@ -298,7 +377,7 @@ App::get('/v1/mock/tests/general/oauth2') }); App::get('/v1/mock/tests/general/oauth2/token') - ->desc('Mock an OAuth2 login route') + ->desc('OAuth2 Token') ->groups(['mock']) ->label('scope', 'public') ->label('docs', false) @@ -327,7 +406,7 @@ App::get('/v1/mock/tests/general/oauth2/token') }); App::get('/v1/mock/tests/general/oauth2/user') - ->desc('Mock an OAuth2 user route') + ->desc('OAuth2 User') ->groups(['mock']) ->label('scope', 'public') ->label('docs', false) @@ -348,8 +427,9 @@ App::get('/v1/mock/tests/general/oauth2/user') }); App::get('/v1/mock/tests/general/oauth2/success') - ->label('scope', 'public') + ->desc('OAuth2 Success') ->groups(['mock']) + ->label('scope', 'public') ->label('docs', false) ->inject('response') ->action(function ($response) { @@ -361,6 +441,7 @@ App::get('/v1/mock/tests/general/oauth2/success') }); App::get('/v1/mock/tests/general/oauth2/failure') + ->desc('OAuth2 Failure') ->groups(['mock']) ->label('scope', 'public') ->label('docs', false) @@ -397,5 +478,5 @@ App::shutdown(function($utopia, $response, $request) { throw new Exception('Failed to save resutls', 500); } - $response->json(['result' => $route->getMethod() . ':' . $route->getURL() . ':passed']); + $response->dynamic(new Document(['result' => $route->getMethod() . ':' . $route->getURL() . ':passed']), Response::MODEL_MOCK); }, ['utopia', 'response', 'request'], 'mock'); \ No newline at end of file diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 1414147e10..8b3971079d 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -78,7 +78,7 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e ->setParam('projectId', $project->getId()) ->setParam('userId', $user->getId()) ->setParam('event', $route->getLabel('event', '')) - ->setParam('payload', []) + ->setParam('eventData', []) ->setParam('functionId', null) ->setParam('executionId', null) ->setParam('trigger', 'event') @@ -178,8 +178,8 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits /** @var bool $mode */ if (!empty($events->getParam('event'))) { - if(empty($events->getParam('payload'))) { - $events->setParam('payload', $response->getPayload()); + if(empty($events->getParam('eventData'))) { + $events->setParam('eventData', $response->getPayload()); } $webhooks = clone $events; diff --git a/app/http.php b/app/http.php index 9e45bc3b52..d144c2e54a 100644 --- a/app/http.php +++ b/app/http.php @@ -94,7 +94,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo return; } - $app = new App('America/New_York'); + $app = new App('UTC'); try { Authorization::cleanRoles(); diff --git a/app/init.php b/app/init.php index f1d7f3e521..387d7f29b5 100644 --- a/app/init.php +++ b/app/init.php @@ -39,8 +39,8 @@ const APP_USERAGENT = APP_NAME.'-Server v%s. Please report abuse at %s'; const APP_MODE_DEFAULT = 'default'; const APP_MODE_ADMIN = 'admin'; const APP_PAGING_LIMIT = 12; -const APP_CACHE_BUSTER = 144; -const APP_VERSION_STABLE = '0.7.0'; +const APP_CACHE_BUSTER = 145; +const APP_VERSION_STABLE = '0.8.0'; const APP_STORAGE_UPLOADS = '/storage/uploads'; const APP_STORAGE_FUNCTIONS = '/storage/functions'; const APP_STORAGE_CACHE = '/storage/cache'; @@ -420,7 +420,7 @@ App::setResource('user', function($mode, $project, $console, $request, $response if (empty($user->getId()) // Check a document has been found in the DB || Database::SYSTEM_COLLECTION_USERS !== $user->getCollection() // Validate returned document is really a user document - || !Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret)) { // Validate user has valid login token + || !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)) { // Validate user has valid login token $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); } diff --git a/app/preload.php b/app/preload.php index f73aaea61a..7c8ae00938 100644 --- a/app/preload.php +++ b/app/preload.php @@ -28,10 +28,9 @@ foreach ([ realpath(__DIR__ . '/../vendor/felixfbecker'), realpath(__DIR__ . '/../vendor/twig/twig'), realpath(__DIR__ . '/../vendor/guzzlehttp/guzzle'), - realpath(__DIR__ . '/../vendor/domnikl'), - realpath(__DIR__ . '/../vendor/domnikl'), + realpath(__DIR__ . '/../vendor/slickdeals'), realpath(__DIR__ . '/../vendor/psr/log'), - realpath(__DIR__ . '/../vendor/piwik'), + realpath(__DIR__ . '/../vendor/matomo'), realpath(__DIR__ . '/../vendor/symfony'), ] as $key => $value) { if($value !== false) { diff --git a/app/tasks/install.php b/app/tasks/install.php index 39ed29a5bb..b1b882a215 100644 --- a/app/tasks/install.php +++ b/app/tasks/install.php @@ -124,7 +124,7 @@ $cli $input[$var['name']] = Console::confirm($var['question'].' (default: \''.$var['default'].'\')'); - if(empty($input[$key])) { + if(empty($input[$var['name']])) { $input[$var['name']] = $var['default']; } } @@ -181,4 +181,4 @@ $cli $analytics->createEvent('install/server', 'install', APP_VERSION_STABLE.' - '.$message); Console::success($message); } - }); \ No newline at end of file + }); diff --git a/app/tasks/sdks.php b/app/tasks/sdks.php index 6b17104f6c..da9d2ae74c 100644 --- a/app/tasks/sdks.php +++ b/app/tasks/sdks.php @@ -42,7 +42,7 @@ $cli $production = ($git) ? (Console::confirm('Type "Appwrite" to push code to production git repos') == 'Appwrite') : false; $message = ($git) ? Console::confirm('Please enter your commit message:') : ''; - if(!in_array($version, ['0.6.2', '0.7.0'])) { + if(!in_array($version, ['0.6.x', '0.7.x'])) { throw new Exception('Unknown version given'); } @@ -67,6 +67,8 @@ $cli $target = \realpath(__DIR__.'/..').'/sdks/git/'.$language['key'].'/'; $readme = \realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/README.md'); $readme = ($readme) ? \file_get_contents($readme) : ''; + $gettingStarted = \realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/GETTING_STARTED.md'); + $gettingStarted = ($gettingStarted) ? \file_get_contents($gettingStarted) : ''; $examples = \realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/EXAMPLES.md'); $examples = ($examples) ? \file_get_contents($examples) : ''; $changelog = \realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/CHANGELOG.md'); @@ -96,6 +98,15 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $config = new CLI(); $config->setComposerVendor('appwrite'); $config->setComposerPackage('cli'); + $config->setExecutableName('appwrite'); + $config->setLogo(" + _ _ _ ___ __ _____ + /_\ _ __ _ ____ ___ __(_) |_ ___ / __\ / / \_ \ + //_\\| '_ \| '_ \ \ /\ / / '__| | __/ _ \ / / / / / /\/ + / _ \ |_) | |_) \ V V /| | | | || __/ / /___/ /___/\/ /_ + \_/ \_/ .__/| .__/ \_/\_/ |_| |_|\__\___| \____/\____/\____/ + |_| |_| + "); break; case 'php': $config = new PHP(); @@ -178,6 +189,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ->setShareVia('appwrite_io') ->setWarning($warning) ->setReadme($readme) + ->setGettingStarted($gettingStarted) ->setChangelog($changelog) ->setExamples($examples) ; diff --git a/app/views/console/functions/function.phtml b/app/views/console/functions/function.phtml index 4ae65f491e..15ba114350 100644 --- a/app/views/console/functions/function.phtml +++ b/app/views/console/functions/function.phtml @@ -50,24 +50,9 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);

- -
-   View Logs -
+
+   View Logs +
@@ -575,6 +560,31 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true); +