Merge branch 'master' into feat-simplify-exceptions
This commit is contained in:
commit
43660d9c8f
11
CHANGES.md
11
CHANGES.md
|
@ -1,3 +1,14 @@
|
|||
# Version 0.15.3
|
||||
## Features
|
||||
- Added hint during Installation for DNS Configuration by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/2450
|
||||
## Bugs
|
||||
- Fixed Migration for Attributes and Indexes by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3568
|
||||
- Fixed Closed Icon in the alerts to be centered by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3594
|
||||
- Fixed Response Model for Get and Update Database Endpoint by @ishanvyas22 in https://github.com/appwrite/appwrite/pull/3553
|
||||
- Fixed Missing Usage on Functions exection by @Meldiron in https://github.com/appwrite/appwrite/pull/3543
|
||||
- Fixed Validation for Permissions to only accept a maximum of 100 Permissions for all endpoints by @Meldiron in https://github.com/appwrite/appwrite/pull/3532
|
||||
- Fixed backwards compatibility for Create Email Session Endpoint by @stnguyen90 in https://github.com/appwrite/appwrite/pull/3517
|
||||
|
||||
# Version 0.15.2
|
||||
## Bugs
|
||||
- Fixed Realtime Authentication for the Console by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3506
|
||||
|
|
|
@ -317,7 +317,7 @@ The Runtimes for all supported cloud functions (multicore builds) can be found a
|
|||
|
||||
For generating a new console SDK follow the next steps:
|
||||
|
||||
1. Update the console spec file located at `app/config/specs/swagger2-0.12.x.console.json` from the dynamic version located at `https://localhost/specs/swagger2?platform=console`
|
||||
1. Update the console spec file located at `app/config/specs/swagger2-<version-number>.console.json` using Appwrite Tasks. Run the `php app/cli.php specs <version-number> normal` command in a running `appwrite/appwrite` container.
|
||||
2. Generate a new SDK using the command `php app/cli.php sdks`
|
||||
3. Change your working dir using `cd app/sdks/console-web`
|
||||
4. Build the new SDK `npm run build`
|
||||
|
@ -462,4 +462,4 @@ Submitting documentation updates, enhancements, designs, or bug fixes. Spelling
|
|||
|
||||
### Helping Someone
|
||||
|
||||
Searching for Appwrite on Discord, GitHub, or StackOverflow and helping someone else who needs help. You can also help by teaching others how to contribute to Appwrite's repo!
|
||||
Searching for Appwrite on Discord, GitHub, or StackOverflow and helping someone else who needs help. You can also help by teaching others how to contribute to Appwrite's repo!
|
||||
|
|
|
@ -59,7 +59,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.15.2
|
||||
appwrite/appwrite:0.15.3
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
@ -71,7 +71,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.15.2
|
||||
appwrite/appwrite:0.15.3
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
@ -81,7 +81,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.15.2
|
||||
appwrite/appwrite:0.15.3
|
||||
```
|
||||
|
||||
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
|
||||
|
|
|
@ -65,7 +65,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.15.2
|
||||
appwrite/appwrite:0.15.3
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
@ -77,7 +77,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.15.2
|
||||
appwrite/appwrite:0.15.3
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
@ -87,7 +87,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.15.2
|
||||
appwrite/appwrite:0.15.3
|
||||
```
|
||||
|
||||
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.
|
||||
|
|
|
@ -180,7 +180,7 @@ return [
|
|||
[
|
||||
'key' => 'cli',
|
||||
'name' => 'Command Line',
|
||||
'version' => '0.18.1',
|
||||
'version' => '0.18.3',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-cli',
|
||||
'package' => 'https://www.npmjs.com/package/appwrite-cli',
|
||||
'enabled' => true,
|
||||
|
|
|
@ -31,6 +31,16 @@ return [ // Ordered by ABC.
|
|||
'beta' => false,
|
||||
'mock' => false,
|
||||
],
|
||||
'authentik' => [
|
||||
'name' => 'Authentik',
|
||||
'developers' => 'https://goauthentik.io/docs/',
|
||||
'icon' => 'icon-authentik',
|
||||
'enabled' => true,
|
||||
'sandbox' => false,
|
||||
'form' => 'authentik.phtml',
|
||||
'beta' => false,
|
||||
'mock' => false,
|
||||
],
|
||||
'autodesk' => [
|
||||
'name' => 'Autodesk',
|
||||
'developers' => 'https://forge.autodesk.com/en/docs/oauth/v2/developers_guide/overview/',
|
||||
|
@ -201,6 +211,16 @@ return [ // Ordered by ABC.
|
|||
'beta' => false,
|
||||
'mock' => false
|
||||
],
|
||||
'podio' => [
|
||||
'name' => 'Podio',
|
||||
'developers' => 'https://developers.podio.com/doc/oauth-authorization',
|
||||
'icon' => 'icon-podio',
|
||||
'enabled' => true,
|
||||
'sandbox' => false,
|
||||
'form' => false,
|
||||
'beta' => false,
|
||||
'mock' => false,
|
||||
],
|
||||
'salesforce' => [
|
||||
'name' => 'Salesforce',
|
||||
'developers' => 'https://developer.salesforce.com/docs/',
|
||||
|
|
|
@ -886,7 +886,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/database#createCollection).')
|
||||
->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('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?')
|
||||
|
@ -932,7 +932,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/database#createCollection).')
|
||||
->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('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)
|
||||
|
@ -972,7 +972,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/database#createCollection).')
|
||||
->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('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?')
|
||||
|
@ -1028,7 +1028,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/database#createCollection).')
|
||||
->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('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,7 +1068,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/database#createCollection).')
|
||||
->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('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)
|
||||
|
@ -1108,7 +1108,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/database#createCollection).')
|
||||
->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('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)
|
||||
|
@ -1177,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/database#createCollection).')
|
||||
->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('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)
|
||||
|
@ -1249,7 +1249,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/database#createCollection).')
|
||||
->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('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)
|
||||
|
@ -1287,7 +1287,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/database#createCollection).')
|
||||
->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).')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('usage')
|
||||
|
@ -1337,7 +1337,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key')
|
|||
Response::MODEL_ATTRIBUTE_IP,
|
||||
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/database#createCollection).')
|
||||
->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('key', '', new Key(), 'Attribute Key.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
|
@ -1400,7 +1400,7 @@ 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/database#createCollection).')
|
||||
->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('key', '', new Key(), 'Attribute Key.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
|
@ -1495,7 +1495,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/database#createCollection).')
|
||||
->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('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.')
|
||||
|
@ -1650,7 +1650,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/database#createCollection).')
|
||||
->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).')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('usage')
|
||||
|
@ -1692,7 +1692,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/database#createCollection).')
|
||||
->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('key', null, new Key(), 'Index Key.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
|
@ -1743,7 +1743,7 @@ 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/database#createCollection).')
|
||||
->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('key', '', new Key(), 'Index Key.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
|
@ -1820,7 +1820,7 @@ 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/database#createCollection). 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/databases#databasesCreateCollection). Make sure to define attributes before creating documents.')
|
||||
->param('data', [], new JSON(), 'Document data as JSON object.')
|
||||
->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
|
||||
->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
|
||||
|
@ -1940,8 +1940,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/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('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('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)
|
||||
|
@ -2054,7 +2054,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/database#createCollection).')
|
||||
->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('documentId', null, new UID(), 'Document ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
|
@ -2357,7 +2357,7 @@ 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/database#createCollection).')
|
||||
->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('documentId', null, new UID(), 'Document ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
|
|
|
@ -31,12 +31,14 @@ use Utopia\Validator\Range;
|
|||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
App::init(function (Document $project) {
|
||||
|
||||
if ($project->getId() !== 'console') {
|
||||
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN);
|
||||
}
|
||||
}, ['project'], 'projects');
|
||||
App::init()
|
||||
->groups(['projects'])
|
||||
->inject('project')
|
||||
->action(function (Document $project) {
|
||||
if ($project->getId() !== 'console') {
|
||||
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN);
|
||||
}
|
||||
});
|
||||
|
||||
App::post('/v1/projects')
|
||||
->desc('Create Project')
|
||||
|
|
|
@ -427,8 +427,8 @@ App::post('/v1/teams/:teamId/memberships')
|
|||
$response->dynamic(
|
||||
$membership
|
||||
->setAttribute('teamName', $team->getAttribute('name'))
|
||||
->setAttribute('userName', $user->getAttribute('name'))
|
||||
->setAttribute('userEmail', $user->getAttribute('email')),
|
||||
->setAttribute('userName', $invitee->getAttribute('name'))
|
||||
->setAttribute('userEmail', $invitee->getAttribute('email')),
|
||||
Response::MODEL_MEMBERSHIP
|
||||
);
|
||||
});
|
||||
|
|
|
@ -35,485 +35,506 @@ Config::setParam('domainVerification', false);
|
|||
Config::setParam('cookieDomain', 'localhost');
|
||||
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
|
||||
|
||||
App::init(function (App $utopia, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $clients) {
|
||||
App::init()
|
||||
->inject('utopia')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('console')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->inject('user')
|
||||
->inject('locale')
|
||||
->inject('clients')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $clients) {
|
||||
/*
|
||||
* Request format
|
||||
*/
|
||||
$route = $utopia->match($request);
|
||||
Request::setRoute($route);
|
||||
|
||||
/*
|
||||
* Request format
|
||||
*/
|
||||
$route = $utopia->match($request);
|
||||
Request::setRoute($route);
|
||||
|
||||
$requestFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($requestFormat) {
|
||||
switch ($requestFormat) {
|
||||
case version_compare($requestFormat, '0.12.0', '<'):
|
||||
Request::setFilter(new RequestV12());
|
||||
break;
|
||||
case version_compare($requestFormat, '0.13.0', '<'):
|
||||
Request::setFilter(new RequestV13());
|
||||
break;
|
||||
case version_compare($requestFormat, '0.14.0', '<'):
|
||||
Request::setFilter(new RequestV14());
|
||||
break;
|
||||
default:
|
||||
Request::setFilter(null);
|
||||
}
|
||||
} else {
|
||||
Request::setFilter(null);
|
||||
}
|
||||
|
||||
$domain = $request->getHostname();
|
||||
$domains = Config::getParam('domains', []);
|
||||
if (!array_key_exists($domain, $domains)) {
|
||||
$domain = new Domain(!empty($domain) ? $domain : '');
|
||||
|
||||
if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) {
|
||||
$domains[$domain->get()] = false;
|
||||
Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.');
|
||||
} elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
|
||||
Console::warning('Skipping SSL certificates generation on ACME challenge.');
|
||||
} else {
|
||||
Authorization::disable();
|
||||
|
||||
$envDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
$mainDomain = null;
|
||||
if (!empty($envDomain) && $envDomain !== 'localhost') {
|
||||
$mainDomain = $envDomain;
|
||||
} else {
|
||||
$domainDocument = $dbForConsole->findOne('domains', [], 0, ['_id'], ['ASC']);
|
||||
$mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get();
|
||||
$requestFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($requestFormat) {
|
||||
switch ($requestFormat) {
|
||||
case version_compare($requestFormat, '0.12.0', '<'):
|
||||
Request::setFilter(new RequestV12());
|
||||
break;
|
||||
case version_compare($requestFormat, '0.13.0', '<'):
|
||||
Request::setFilter(new RequestV13());
|
||||
break;
|
||||
case version_compare($requestFormat, '0.14.0', '<'):
|
||||
Request::setFilter(new RequestV14());
|
||||
break;
|
||||
default:
|
||||
Request::setFilter(null);
|
||||
}
|
||||
} else {
|
||||
Request::setFilter(null);
|
||||
}
|
||||
|
||||
if ($mainDomain !== $domain->get()) {
|
||||
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
|
||||
$domain = $request->getHostname();
|
||||
$domains = Config::getParam('domains', []);
|
||||
if (!array_key_exists($domain, $domains)) {
|
||||
$domain = new Domain(!empty($domain) ? $domain : '');
|
||||
|
||||
if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) {
|
||||
$domains[$domain->get()] = false;
|
||||
Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.');
|
||||
} elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
|
||||
Console::warning('Skipping SSL certificates generation on ACME challenge.');
|
||||
} else {
|
||||
$domainDocument = $dbForConsole->findOne('domains', [
|
||||
new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()])
|
||||
]);
|
||||
Authorization::disable();
|
||||
|
||||
if (!$domainDocument) {
|
||||
$domainDocument = new Document([
|
||||
'domain' => $domain->get(),
|
||||
'tld' => $domain->getSuffix(),
|
||||
'registerable' => $domain->getRegisterable(),
|
||||
'verification' => false,
|
||||
'certificateId' => null,
|
||||
$envDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
$mainDomain = null;
|
||||
if (!empty($envDomain) && $envDomain !== 'localhost') {
|
||||
$mainDomain = $envDomain;
|
||||
} else {
|
||||
$domainDocument = $dbForConsole->findOne('domains', [], 0, ['_id'], ['ASC']);
|
||||
$mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get();
|
||||
}
|
||||
|
||||
if ($mainDomain !== $domain->get()) {
|
||||
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
|
||||
} else {
|
||||
$domainDocument = $dbForConsole->findOne('domains', [
|
||||
new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()])
|
||||
]);
|
||||
|
||||
$domainDocument = $dbForConsole->createDocument('domains', $domainDocument);
|
||||
if (!$domainDocument) {
|
||||
$domainDocument = new Document([
|
||||
'domain' => $domain->get(),
|
||||
'tld' => $domain->getSuffix(),
|
||||
'registerable' => $domain->getRegisterable(),
|
||||
'verification' => false,
|
||||
'certificateId' => null,
|
||||
]);
|
||||
|
||||
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
|
||||
$domainDocument = $dbForConsole->createDocument('domains', $domainDocument);
|
||||
|
||||
(new Certificate())
|
||||
->setDomain($domainDocument)
|
||||
->trigger();
|
||||
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
|
||||
|
||||
(new Certificate())
|
||||
->setDomain($domainDocument)
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
$domains[$domain->get()] = true;
|
||||
|
||||
Authorization::reset(); // ensure authorization is re-enabled
|
||||
}
|
||||
$domains[$domain->get()] = true;
|
||||
|
||||
Authorization::reset(); // ensure authorization is re-enabled
|
||||
}
|
||||
Config::setParam('domains', $domains);
|
||||
}
|
||||
|
||||
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
|
||||
if (\in_array($localeParam, Config::getParam('locale-codes'))) {
|
||||
$locale->setDefault($localeParam);
|
||||
}
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!empty($route->getLabel('sdk.auth', [])) && $project->isEmpty() && ($route->getLabel('scope', '') !== 'public')) {
|
||||
throw new AppwriteException(AppwriteException::PROJECT_UNKNOWN);
|
||||
}
|
||||
|
||||
$referrer = $request->getReferer();
|
||||
$origin = \parse_url($request->getOrigin($referrer), PHP_URL_HOST);
|
||||
$protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME);
|
||||
$port = \parse_url($request->getOrigin($referrer), PHP_URL_PORT);
|
||||
|
||||
$refDomainOrigin = 'localhost';
|
||||
$validator = new Hostname($clients);
|
||||
if ($validator->isValid($origin)) {
|
||||
$refDomainOrigin = $origin;
|
||||
}
|
||||
|
||||
$refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $refDomainOrigin . (!empty($port) ? ':' . $port : '');
|
||||
|
||||
$refDomain = (!$route->getLabel('origin', false)) // This route is publicly accessible
|
||||
? $refDomain
|
||||
: (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
|
||||
|
||||
$selfDomain = new Domain($request->getHostname());
|
||||
$endDomain = new Domain((string)$origin);
|
||||
|
||||
Config::setParam(
|
||||
'domainVerification',
|
||||
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
|
||||
$endDomain->getRegisterable() !== ''
|
||||
);
|
||||
|
||||
Config::setParam('cookieDomain', (
|
||||
$request->getHostname() === 'localhost' ||
|
||||
$request->getHostname() === 'localhost:' . $request->getPort() ||
|
||||
(\filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false)
|
||||
)
|
||||
? null
|
||||
: '.' . $request->getHostname());
|
||||
|
||||
/*
|
||||
* Response format
|
||||
*/
|
||||
$responseFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($responseFormat) {
|
||||
switch ($responseFormat) {
|
||||
case version_compare($responseFormat, '0.11.2', '<='):
|
||||
Response::setFilter(new ResponseV11());
|
||||
break;
|
||||
case version_compare($responseFormat, '0.12.4', '<='):
|
||||
Response::setFilter(new ResponseV12());
|
||||
break;
|
||||
case version_compare($responseFormat, '0.13.4', '<='):
|
||||
Response::setFilter(new ResponseV13());
|
||||
break;
|
||||
case version_compare($responseFormat, '0.14.0', '<='):
|
||||
Response::setFilter(new ResponseV14());
|
||||
break;
|
||||
default:
|
||||
Response::setFilter(null);
|
||||
}
|
||||
} else {
|
||||
Response::setFilter(null);
|
||||
}
|
||||
|
||||
/*
|
||||
* Security Headers
|
||||
*
|
||||
* As recommended at:
|
||||
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
|
||||
*/
|
||||
if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
|
||||
if ($request->getProtocol() !== 'https') {
|
||||
if ($request->getMethod() !== Request::METHOD_GET) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED);
|
||||
}
|
||||
|
||||
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
Config::setParam('domains', $domains);
|
||||
}
|
||||
|
||||
$response->addHeader('Strict-Transport-Security', 'max-age=' . (60 * 60 * 24 * 126)); // 126 days
|
||||
}
|
||||
|
||||
$response
|
||||
->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-Appwrite-Response-Format, X-SDK-Version, X-Appwrite-ID, Content-Range, Range, Cache-Control, Expires, Pragma')
|
||||
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
|
||||
->addHeader('Access-Control-Allow-Origin', $refDomain)
|
||||
->addHeader('Access-Control-Allow-Credentials', 'true')
|
||||
;
|
||||
|
||||
/*
|
||||
* Validate Client Domain - Check to avoid CSRF attack
|
||||
* Adding Appwrite API domains to allow XDOMAIN communication
|
||||
* Skip this check for non-web platforms which are not required to send an origin header
|
||||
*/
|
||||
$origin = $request->getOrigin($request->getReferer(''));
|
||||
$originValidator = new Origin(\array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', [])));
|
||||
|
||||
if (
|
||||
!$originValidator->isValid($origin)
|
||||
&& \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE])
|
||||
&& $route->getLabel('origin', false) !== '*'
|
||||
&& empty($request->getHeader('x-appwrite-key', ''))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_UNKNOWN_ORIGIN, $originValidator->getDescription());
|
||||
}
|
||||
|
||||
/*
|
||||
* ACL Check
|
||||
*/
|
||||
$role = ($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER;
|
||||
|
||||
// Add user roles
|
||||
$memberships = $user->find('teamId', $project->getAttribute('teamId', null), 'memberships');
|
||||
|
||||
if ($memberships) {
|
||||
foreach ($memberships->getAttribute('roles', []) as $memberRole) {
|
||||
switch ($memberRole) {
|
||||
case 'owner':
|
||||
$role = Auth::USER_ROLE_OWNER;
|
||||
break;
|
||||
case 'admin':
|
||||
$role = Auth::USER_ROLE_ADMIN;
|
||||
break;
|
||||
case 'developer':
|
||||
$role = Auth::USER_ROLE_DEVELOPER;
|
||||
break;
|
||||
}
|
||||
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
|
||||
if (\in_array($localeParam, Config::getParam('locale-codes'))) {
|
||||
$locale->setDefault($localeParam);
|
||||
}
|
||||
}
|
||||
|
||||
$roles = Config::getParam('roles', []);
|
||||
$scope = $route->getLabel('scope', 'none'); // Allowed scope for chosen route
|
||||
$scopes = $roles[$role]['scopes']; // Allowed scopes for user role
|
||||
if ($project->isEmpty()) {
|
||||
throw new AppwriteException('Project not found', 404, AppwriteException::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$authKey = $request->getHeader('x-appwrite-key', '');
|
||||
if (!empty($route->getLabel('sdk.auth', [])) && $project->isEmpty() && ($route->getLabel('scope', '') !== 'public')) {
|
||||
throw new AppwriteException('Missing or unknown project ID', 400, AppwriteException::PROJECT_UNKNOWN);
|
||||
}
|
||||
|
||||
if (!empty($authKey)) { // API Key authentication
|
||||
// Check if given key match project API keys
|
||||
$key = $project->find('secret', $authKey, 'keys');
|
||||
$referrer = $request->getReferer();
|
||||
$origin = \parse_url($request->getOrigin($referrer), PHP_URL_HOST);
|
||||
$protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME);
|
||||
$port = \parse_url($request->getOrigin($referrer), PHP_URL_PORT);
|
||||
|
||||
$refDomainOrigin = 'localhost';
|
||||
$validator = new Hostname($clients);
|
||||
if ($validator->isValid($origin)) {
|
||||
$refDomainOrigin = $origin;
|
||||
}
|
||||
|
||||
$refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $refDomainOrigin . (!empty($port) ? ':' . $port : '');
|
||||
|
||||
$refDomain = (!$route->getLabel('origin', false)) // This route is publicly accessible
|
||||
? $refDomain
|
||||
: (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
|
||||
|
||||
$selfDomain = new Domain($request->getHostname());
|
||||
$endDomain = new Domain((string)$origin);
|
||||
|
||||
Config::setParam(
|
||||
'domainVerification',
|
||||
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
|
||||
$endDomain->getRegisterable() !== ''
|
||||
);
|
||||
|
||||
Config::setParam('cookieDomain', (
|
||||
$request->getHostname() === 'localhost' ||
|
||||
$request->getHostname() === 'localhost:' . $request->getPort() ||
|
||||
(\filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false)
|
||||
)
|
||||
? null
|
||||
: '.' . $request->getHostname());
|
||||
|
||||
/*
|
||||
* Try app auth when we have project key and no user
|
||||
* Mock user to app and grant API key scopes in addition to default app scopes
|
||||
*/
|
||||
if ($key && $user->isEmpty()) {
|
||||
$user = new Document([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => $project->getAttribute('name', 'Untitled'),
|
||||
]);
|
||||
* Response format
|
||||
*/
|
||||
$responseFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($responseFormat) {
|
||||
switch ($responseFormat) {
|
||||
case version_compare($responseFormat, '0.11.2', '<='):
|
||||
Response::setFilter(new ResponseV11());
|
||||
break;
|
||||
case version_compare($responseFormat, '0.12.4', '<='):
|
||||
Response::setFilter(new ResponseV12());
|
||||
break;
|
||||
case version_compare($responseFormat, '0.13.4', '<='):
|
||||
Response::setFilter(new ResponseV13());
|
||||
break;
|
||||
case version_compare($responseFormat, '0.14.0', '<='):
|
||||
Response::setFilter(new ResponseV14());
|
||||
break;
|
||||
default:
|
||||
Response::setFilter(null);
|
||||
}
|
||||
} else {
|
||||
Response::setFilter(null);
|
||||
}
|
||||
|
||||
$role = Auth::USER_ROLE_APP;
|
||||
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
|
||||
/*
|
||||
* Security Headers
|
||||
*
|
||||
* As recommended at:
|
||||
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
|
||||
*/
|
||||
if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
|
||||
if ($request->getProtocol() !== 'https') {
|
||||
if ($request->getMethod() !== Request::METHOD_GET) {
|
||||
throw new AppwriteException('Method unsupported over HTTP.', 500, AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$expire = $key->getAttribute('expire', 0);
|
||||
|
||||
if (!empty($expire) && $expire < \time()) {
|
||||
throw new AppwriteException(AppwriteException::PROJECT_KEY_EXPIRED);
|
||||
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
}
|
||||
|
||||
Authorization::setRole('role:' . Auth::USER_ROLE_APP);
|
||||
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
|
||||
$response->addHeader('Strict-Transport-Security', 'max-age=' . (60 * 60 * 24 * 126)); // 126 days
|
||||
}
|
||||
}
|
||||
|
||||
Authorization::setRole('role:' . $role);
|
||||
$response
|
||||
->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-Appwrite-Response-Format, X-SDK-Version, X-Appwrite-ID, Content-Range, Range, Cache-Control, Expires, Pragma')
|
||||
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
|
||||
->addHeader('Access-Control-Allow-Origin', $refDomain)
|
||||
->addHeader('Access-Control-Allow-Credentials', 'true')
|
||||
;
|
||||
|
||||
foreach (Auth::getRoles($user) as $authRole) {
|
||||
Authorization::setRole($authRole);
|
||||
}
|
||||
/*
|
||||
* Validate Client Domain - Check to avoid CSRF attack
|
||||
* Adding Appwrite API domains to allow XDOMAIN communication
|
||||
* Skip this check for non-web platforms which are not required to send an origin header
|
||||
*/
|
||||
$origin = $request->getOrigin($request->getReferer(''));
|
||||
$originValidator = new Origin(\array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', [])));
|
||||
|
||||
$service = $route->getLabel('sdk.namespace', '');
|
||||
if (!empty($service)) {
|
||||
if (
|
||||
array_key_exists($service, $project->getAttribute('services', []))
|
||||
&& !$project->getAttribute('services', [])[$service]
|
||||
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
|
||||
!$originValidator->isValid($origin)
|
||||
&& \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE])
|
||||
&& $route->getLabel('origin', false) !== '*'
|
||||
&& empty($request->getHeader('x-appwrite-key', ''))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_SERVICE_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
if (!\in_array($scope, $scopes)) {
|
||||
if ($project->isEmpty()) { // Check if permission is denied because project is missing
|
||||
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
|
||||
throw new AppwriteException($originValidator->getDescription(), 403, AppwriteException::GENERAL_UNKNOWN_ORIGIN);
|
||||
}
|
||||
|
||||
throw new AppwriteException(AppwriteException::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')');
|
||||
}
|
||||
/*
|
||||
* ACL Check
|
||||
*/
|
||||
$role = ($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER;
|
||||
|
||||
if (false === $user->getAttribute('status')) { // Account is blocked
|
||||
throw new AppwriteException(AppwriteException::USER_BLOCKED);
|
||||
}
|
||||
// Add user roles
|
||||
$memberships = $user->find('teamId', $project->getAttribute('teamId', null), 'memberships');
|
||||
|
||||
if ($user->getAttribute('reset')) {
|
||||
throw new AppwriteException(AppwriteException::USER_PASSWORD_RESET_REQUIRED);
|
||||
}
|
||||
}, ['utopia', 'request', 'response', 'console', 'project', 'dbForConsole', 'user', 'locale', 'clients']);
|
||||
if ($memberships) {
|
||||
foreach ($memberships->getAttribute('roles', []) as $memberRole) {
|
||||
switch ($memberRole) {
|
||||
case 'owner':
|
||||
$role = Auth::USER_ROLE_OWNER;
|
||||
break;
|
||||
case 'admin':
|
||||
$role = Auth::USER_ROLE_ADMIN;
|
||||
break;
|
||||
case 'developer':
|
||||
$role = Auth::USER_ROLE_DEVELOPER;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
App::options(function (Request $request, Response $response) {
|
||||
$roles = Config::getParam('roles', []);
|
||||
$scope = $route->getLabel('scope', 'none'); // Allowed scope for chosen route
|
||||
$scopes = $roles[$role]['scopes']; // Allowed scopes for user role
|
||||
|
||||
$origin = $request->getOrigin();
|
||||
$authKey = $request->getHeader('x-appwrite-key', '');
|
||||
|
||||
$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-Appwrite-Response-Format, X-SDK-Version, X-Appwrite-ID, Content-Range, Range, 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')
|
||||
->noContent();
|
||||
}, ['request', 'response']);
|
||||
if (!empty($authKey)) { // API Key authentication
|
||||
// Check if given key match project API keys
|
||||
$key = $project->find('secret', $authKey, 'keys');
|
||||
|
||||
App::error(function (Throwable $error, App $utopia, Request $request, Response $response, View $layout, Document $project, ?Logger $logger, array $loggerBreadcrumbs) {
|
||||
/*
|
||||
* Try app auth when we have project key and no user
|
||||
* Mock user to app and grant API key scopes in addition to default app scopes
|
||||
*/
|
||||
if ($key && $user->isEmpty()) {
|
||||
$user = new Document([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => $project->getAttribute('name', 'Untitled'),
|
||||
]);
|
||||
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->match($request);
|
||||
$role = Auth::USER_ROLE_APP;
|
||||
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
|
||||
|
||||
/** Delegate PDO exceptions to the global handler so the database connection can be returned to the pool */
|
||||
if ($error instanceof PDOException) {
|
||||
throw $error;
|
||||
}
|
||||
$expire = $key->getAttribute('expire', 0);
|
||||
|
||||
if ($logger) {
|
||||
if ($error->getCode() >= 500 || $error->getCode() === 0) {
|
||||
try {
|
||||
/** @var Utopia\Database\Document $user */
|
||||
$user = $utopia->getResource('user');
|
||||
} catch (\Throwable $th) {
|
||||
// All good, user is optional information for logger
|
||||
if (!empty($expire) && $expire < \time()) {
|
||||
throw new AppwriteException('Project key expired', 401, AppwriteException:: PROJECT_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
Authorization::setRole('role:' . Auth::USER_ROLE_APP);
|
||||
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
|
||||
}
|
||||
}
|
||||
|
||||
Authorization::setRole('role:' . $role);
|
||||
|
||||
foreach (Auth::getRoles($user) as $authRole) {
|
||||
Authorization::setRole($authRole);
|
||||
}
|
||||
|
||||
$service = $route->getLabel('sdk.namespace', '');
|
||||
if (!empty($service)) {
|
||||
if (
|
||||
array_key_exists($service, $project->getAttribute('services', []))
|
||||
&& !$project->getAttribute('services', [])[$service]
|
||||
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
|
||||
) {
|
||||
throw new AppwriteException('Service is disabled', 503, AppwriteException::GENERAL_SERVICE_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
if (!\in_array($scope, $scopes)) {
|
||||
if ($project->isEmpty()) { // Check if permission is denied because project is missing
|
||||
throw new AppwriteException('Project not found', 404, AppwriteException::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$log = new Utopia\Logger\Log();
|
||||
|
||||
if (isset($user) && !$user->isEmpty()) {
|
||||
$log->setUser(new User($user->getId()));
|
||||
}
|
||||
|
||||
$log->setNamespace("http");
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('method', $route->getMethod());
|
||||
$log->addTag('url', $route->getPath());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('projectId', $project->getId());
|
||||
$log->addTag('hostname', $request->getHostname());
|
||||
$log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
$log->addExtra('roles', Authorization::$roles);
|
||||
|
||||
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
foreach ($loggerBreadcrumbs as $loggerBreadcrumb) {
|
||||
$log->addBreadcrumb($loggerBreadcrumb);
|
||||
}
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Log pushed with status code: ' . $responseCode);
|
||||
}
|
||||
}
|
||||
|
||||
$code = $error->getCode();
|
||||
$message = $error->getMessage();
|
||||
$file = $error->getFile();
|
||||
$line = $error->getLine();
|
||||
$trace = $error->getTrace();
|
||||
|
||||
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->getPath());
|
||||
throw new AppwriteException($user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')', 401, AppwriteException::GENERAL_UNAUTHORIZED_SCOPE);
|
||||
}
|
||||
|
||||
Console::error('[Error] Type: ' . get_class($error));
|
||||
Console::error('[Error] Message: ' . $message);
|
||||
Console::error('[Error] File: ' . $file);
|
||||
Console::error('[Error] Line: ' . $line);
|
||||
}
|
||||
if (false === $user->getAttribute('status')) { // Account is blocked
|
||||
throw new AppwriteException('Invalid credentials. User is blocked', 401, AppwriteException::USER_BLOCKED);
|
||||
}
|
||||
|
||||
/** Handle Utopia Errors */
|
||||
if ($error instanceof Utopia\Exception) {
|
||||
$error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error);
|
||||
switch ($code) {
|
||||
case 400:
|
||||
$error->setType(AppwriteException::GENERAL_ARGUMENT_INVALID);
|
||||
break;
|
||||
case 404:
|
||||
$error->setType(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
if ($user->getAttribute('reset')) {
|
||||
throw new AppwriteException('Password reset is required', 412, AppwriteException::USER_PASSWORD_RESET_REQUIRED);
|
||||
}
|
||||
});
|
||||
|
||||
App::options()
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->action(function (Request $request, Response $response) {
|
||||
|
||||
$origin = $request->getOrigin();
|
||||
|
||||
$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-Appwrite-Response-Format, X-SDK-Version, X-Appwrite-ID, Content-Range, Range, 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')
|
||||
->noContent();
|
||||
});
|
||||
|
||||
App::error()
|
||||
->inject('error')
|
||||
->inject('utopia')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('layout')
|
||||
->inject('project')
|
||||
->inject('logger')
|
||||
->inject('loggerBreadcrumbs')
|
||||
->action(function (Throwable $error, App $utopia, Request $request, Response $response, View $layout, Document $project, ?Logger $logger, array $loggerBreadcrumbs) {
|
||||
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->match($request);
|
||||
|
||||
/** Delegate PDO exceptions to the global handler so the database connection can be returned to the pool */
|
||||
if ($error instanceof PDOException) {
|
||||
throw $error;
|
||||
}
|
||||
|
||||
if ($logger) {
|
||||
if ($error->getCode() >= 500 || $error->getCode() === 0) {
|
||||
try {
|
||||
/** @var Utopia\Database\Document $user */
|
||||
$user = $utopia->getResource('user');
|
||||
} catch (\Throwable $th) {
|
||||
// All good, user is optional information for logger
|
||||
}
|
||||
|
||||
$log = new Utopia\Logger\Log();
|
||||
|
||||
if (isset($user) && !$user->isEmpty()) {
|
||||
$log->setUser(new User($user->getId()));
|
||||
}
|
||||
|
||||
$log->setNamespace("http");
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('method', $route->getMethod());
|
||||
$log->addTag('url', $route->getPath());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('projectId', $project->getId());
|
||||
$log->addTag('hostname', $request->getHostname());
|
||||
$log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
$log->addExtra('roles', Authorization::$roles);
|
||||
|
||||
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
foreach ($loggerBreadcrumbs as $loggerBreadcrumb) {
|
||||
$log->addBreadcrumb($loggerBreadcrumb);
|
||||
}
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Log pushed with status code: ' . $responseCode);
|
||||
}
|
||||
}
|
||||
|
||||
$code = $error->getCode();
|
||||
$message = $error->getMessage();
|
||||
$file = $error->getFile();
|
||||
$line = $error->getLine();
|
||||
$trace = $error->getTrace();
|
||||
|
||||
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->getPath());
|
||||
}
|
||||
|
||||
Console::error('[Error] Type: ' . get_class($error));
|
||||
Console::error('[Error] Message: ' . $message);
|
||||
Console::error('[Error] File: ' . $file);
|
||||
Console::error('[Error] Line: ' . $line);
|
||||
}
|
||||
|
||||
/** Handle Utopia Errors */
|
||||
if ($error instanceof Utopia\Exception) {
|
||||
$error = new AppwriteException($message, $code, AppwriteException::GENERAL_UNKNOWN, $error);
|
||||
switch ($code) {
|
||||
case 400:
|
||||
$error->setType(AppwriteException::GENERAL_ARGUMENT_INVALID);
|
||||
break;
|
||||
case 404:
|
||||
$error->setType(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** Wrap all exceptions inside Appwrite\Extend\Exception */
|
||||
if (!($error instanceof AppwriteException)) {
|
||||
$error = new AppwriteException($message, $code, AppwriteException::GENERAL_UNKNOWN, $error);
|
||||
}
|
||||
|
||||
switch ($code) { // Don't show 500 errors!
|
||||
case 400: // Error allowed publicly
|
||||
case 401: // Error allowed publicly
|
||||
case 402: // Error allowed publicly
|
||||
case 403: // Error allowed publicly
|
||||
case 404: // Error allowed publicly
|
||||
case 409: // Error allowed publicly
|
||||
case 412: // Error allowed publicly
|
||||
case 416: // Error allowed publicly
|
||||
case 429: // Error allowed publicly
|
||||
case 501: // Error allowed publicly
|
||||
case 503: // Error allowed publicly
|
||||
break;
|
||||
default:
|
||||
$code = 500; // All other errors get the generic 500 server error status code
|
||||
$message = 'Server Error';
|
||||
}
|
||||
}
|
||||
|
||||
/** Wrap all exceptions inside Appwrite\Extend\Exception */
|
||||
if (!($error instanceof AppwriteException)) {
|
||||
$error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error);
|
||||
}
|
||||
//$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised
|
||||
|
||||
switch ($code) { // Don't show 500 errors!
|
||||
case 400: // Error allowed publicly
|
||||
case 401: // Error allowed publicly
|
||||
case 402: // Error allowed publicly
|
||||
case 403: // Error allowed publicly
|
||||
case 404: // Error allowed publicly
|
||||
case 409: // Error allowed publicly
|
||||
case 412: // Error allowed publicly
|
||||
case 416: // Error allowed publicly
|
||||
case 429: // Error allowed publicly
|
||||
case 501: // Error allowed publicly
|
||||
case 503: // Error allowed publicly
|
||||
break;
|
||||
default:
|
||||
$code = 500; // All other errors get the generic 500 server error status code
|
||||
$message = 'Server Error';
|
||||
}
|
||||
$type = $error->getType();
|
||||
|
||||
//$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised
|
||||
$output = ((App::isDevelopment())) ? [
|
||||
'message' => $message,
|
||||
'code' => $code,
|
||||
'file' => $file,
|
||||
'line' => $line,
|
||||
'trace' => $trace,
|
||||
'version' => $version,
|
||||
'type' => $type,
|
||||
] : [
|
||||
'message' => $message,
|
||||
'code' => $code,
|
||||
'version' => $version,
|
||||
'type' => $type,
|
||||
];
|
||||
|
||||
$type = $error->getType();
|
||||
|
||||
$output = ((App::isDevelopment())) ? [
|
||||
'message' => $message,
|
||||
'code' => $code,
|
||||
'file' => $file,
|
||||
'line' => $line,
|
||||
'trace' => $trace,
|
||||
'version' => $version,
|
||||
'type' => $type,
|
||||
] : [
|
||||
'message' => $message,
|
||||
'code' => $code,
|
||||
'version' => $version,
|
||||
'type' => $type,
|
||||
];
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Expires', '0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->setStatusCode($code)
|
||||
;
|
||||
|
||||
$template = ($route) ? $route->getLabel('error', null) : null;
|
||||
|
||||
if ($template) {
|
||||
$comp = new View($template);
|
||||
|
||||
$comp
|
||||
->setParam('development', App::isDevelopment())
|
||||
->setParam('projectName', $project->getAttribute('name'))
|
||||
->setParam('projectURL', $project->getAttribute('url'))
|
||||
->setParam('message', $error->getMessage())
|
||||
->setParam('code', $code)
|
||||
->setParam('trace', $trace)
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Expires', '0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->setStatusCode($code)
|
||||
;
|
||||
|
||||
$layout
|
||||
->setParam('title', $project->getAttribute('name') . ' - Error')
|
||||
->setParam('description', 'No Description')
|
||||
->setParam('body', $comp)
|
||||
->setParam('version', $version)
|
||||
->setParam('litespeed', false)
|
||||
;
|
||||
$template = ($route) ? $route->getLabel('error', null) : null;
|
||||
|
||||
$response->html($layout->render());
|
||||
}
|
||||
if ($template) {
|
||||
$comp = new View($template);
|
||||
|
||||
$response->dynamic(
|
||||
new Document($output),
|
||||
$utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR
|
||||
);
|
||||
}, ['error', 'utopia', 'request', 'response', 'layout', 'project', 'logger', 'loggerBreadcrumbs']);
|
||||
$comp
|
||||
->setParam('development', App::isDevelopment())
|
||||
->setParam('projectName', $project->getAttribute('name'))
|
||||
->setParam('projectURL', $project->getAttribute('url'))
|
||||
->setParam('message', $error->getMessage())
|
||||
->setParam('code', $code)
|
||||
->setParam('trace', $trace)
|
||||
;
|
||||
|
||||
$layout
|
||||
->setParam('title', $project->getAttribute('name') . ' - Error')
|
||||
->setParam('description', 'No Description')
|
||||
->setParam('body', $comp)
|
||||
->setParam('version', $version)
|
||||
->setParam('litespeed', false)
|
||||
;
|
||||
|
||||
$response->html($layout->render());
|
||||
}
|
||||
|
||||
$response->dynamic(
|
||||
new Document($output),
|
||||
$utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR
|
||||
);
|
||||
});
|
||||
|
||||
App::get('/manifest.json')
|
||||
->desc('Progressive app manifest file')
|
||||
|
@ -580,32 +601,32 @@ App::get('/.well-known/acme-challenge')
|
|||
]);
|
||||
|
||||
if (!$validator->isValid($token) || \count($uriChunks) !== 4) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_QUERY_INVALID, 'Invalid challenge token.');
|
||||
throw new AppwriteException('Invalid challenge token.', 400);
|
||||
}
|
||||
|
||||
$base = \realpath(APP_STORAGE_CERTIFICATES);
|
||||
$absolute = \realpath($base . '/.well-known/acme-challenge/' . $token);
|
||||
|
||||
if (!$base) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Storage error');
|
||||
throw new AppwriteException('Storage error', 500, AppwriteException::GENERAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
if (!$absolute) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, 'Unknown path');
|
||||
throw new AppwriteException('Unknown path', 404);
|
||||
}
|
||||
|
||||
if (!\substr($absolute, 0, \strlen($base)) === $base) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Unknown path');
|
||||
throw new AppwriteException('Invalid path', 401);
|
||||
}
|
||||
|
||||
if (!\file_exists($absolute)) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, 'Unknown path');
|
||||
throw new AppwriteException('Unknown path', 404);
|
||||
}
|
||||
|
||||
$content = @\file_get_contents($absolute);
|
||||
|
||||
if (!$content) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Failed to get contents');
|
||||
throw new AppwriteException('Failed to get contents', 500, AppwriteException::GENERAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$response->text($content);
|
||||
|
|
|
@ -214,7 +214,7 @@ App::get('/v1/mock/tests/general/download')
|
|||
->addHeader('Content-Disposition', 'attachment; filename="test.txt"')
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
|
||||
->addHeader('X-Peak', \memory_get_peak_usage())
|
||||
->send("Download test passed.")
|
||||
->send("GET:/v1/mock/tests/general/download:passed")
|
||||
;
|
||||
});
|
||||
|
||||
|
@ -253,31 +253,31 @@ App::post('/v1/mock/tests/general/upload')
|
|||
$file['size'] = (\is_array($file['size'])) ? $file['size'][0] : $file['size'];
|
||||
|
||||
if (is_null($start) || is_null($end) || is_null($size)) {
|
||||
throw new Exception(Exception::MOCK_INVALID_CONTENT_RANGE_HEADER);
|
||||
throw new Exception('Invalid content-range header', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
if ($start > $end || $end > $size) {
|
||||
throw new Exception(Exception::MOCK_INVALID_CONTENT_RANGE_HEADER);
|
||||
throw new Exception('Invalid content-range header', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
if ($start === 0 && !empty($id)) {
|
||||
throw new Exception(Exception::MOCK_FIRST_CHUNK_CANNOT_HAVE_ID);
|
||||
throw new Exception('First chunked request cannot have id header', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
if ($start !== 0 && $id !== 'newfileid') {
|
||||
throw new Exception(Exception::MOCK_CHUNK_MISSING_ID);
|
||||
throw new Exception('All chunked request must have id header (except first)', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
if ($end !== $size && $end - $start + 1 !== $chunkSize) {
|
||||
throw new Exception(Exception::MOCK_CHUNK_INVALID_SIZE);
|
||||
throw new Exception('Chunk size must be 5MB (except last chunk)', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
if ($end !== $size && $file['size'] !== $chunkSize) {
|
||||
throw new Exception(Exception::MOCK_CHUNK_INVALID_SIZE);
|
||||
throw new Exception('Wrong chunk size', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
if ($file['size'] > $chunkSize) {
|
||||
throw new Exception(Exception::MOCK_CHUNK_INVALID_SIZE);
|
||||
throw new Exception('Chunk size must be 5MB or less', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
if ($end !== $size) {
|
||||
|
@ -293,15 +293,15 @@ App::post('/v1/mock/tests/general/upload')
|
|||
$file['size'] = (\is_array($file['size'])) ? $file['size'][0] : $file['size'];
|
||||
|
||||
if ($file['name'] !== 'file.png') {
|
||||
throw new Exception(Exception::MOCK_INVALID_FILE_NAME);
|
||||
throw new Exception('Wrong file name', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
if ($file['size'] !== 38756) {
|
||||
throw new Exception(Exception::MOCK_INVALID_FILE_SIZE);
|
||||
throw new Exception('Wrong file size', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
if (\md5(\file_get_contents($file['tmp_name'])) !== 'd80e7e6999a3eb2ae0d631a96fe135a4') {
|
||||
throw new Exception(Exception::MOCK_WRONG_FILE_UPLOADED);
|
||||
throw new Exception('Wrong file uploaded', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -374,7 +374,7 @@ App::get('/v1/mock/tests/general/get-cookie')
|
|||
->action(function (Request $request) {
|
||||
|
||||
if ($request->getCookie('cookieName', '') !== 'cookieValue') {
|
||||
throw new Exception(Exception::MOCK_MISSING_COOKIE);
|
||||
throw new Exception('Missing cookie value', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -408,7 +408,7 @@ App::get('/v1/mock/tests/general/400-error')
|
|||
->label('sdk.response.model', Response::MODEL_ERROR)
|
||||
->label('sdk.mock', true)
|
||||
->action(function () {
|
||||
throw new Exception(Exception::MOCK_400);
|
||||
throw new Exception('Mock 400 error', 400, Exception::GENERAL_MOCK);
|
||||
});
|
||||
|
||||
App::get('/v1/mock/tests/general/500-error')
|
||||
|
@ -424,7 +424,7 @@ App::get('/v1/mock/tests/general/500-error')
|
|||
->label('sdk.response.model', Response::MODEL_ERROR)
|
||||
->label('sdk.mock', true)
|
||||
->action(function () {
|
||||
throw new Exception(Exception::MOCK_500);
|
||||
throw new Exception('Mock 500 error', 500, Exception::GENERAL_MOCK);
|
||||
});
|
||||
|
||||
App::get('/v1/mock/tests/general/502-error')
|
||||
|
@ -480,11 +480,11 @@ App::get('/v1/mock/tests/general/oauth2/token')
|
|||
->action(function (string $client_id, string $client_secret, string $grantType, string $redirectURI, string $code, string $refreshToken, Response $response) {
|
||||
|
||||
if ($client_id != '1') {
|
||||
throw new Exception(Exception::MOCK_INVALID_CLIENT_ID);
|
||||
throw new Exception('Invalid client ID', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
if ($client_secret != '123456') {
|
||||
throw new Exception(Exception::MOCK_INVALID_CLIENT_SECRET);
|
||||
throw new Exception('Invalid client secret', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
$responseJson = [
|
||||
|
@ -495,18 +495,18 @@ App::get('/v1/mock/tests/general/oauth2/token')
|
|||
|
||||
if ($grantType === 'authorization_code') {
|
||||
if ($code !== 'abcdef') {
|
||||
throw new Exception(Exception::MOCK_INVALID_TOKEN);
|
||||
throw new Exception('Invalid token', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
$response->json($responseJson);
|
||||
} elseif ($grantType === 'refresh_token') {
|
||||
if ($refreshToken !== 'tuvwxyz') {
|
||||
throw new Exception(Exception::MOCK_INVALID_REFRESH_TOKEN);
|
||||
throw new Exception('Invalid refresh token', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
$response->json($responseJson);
|
||||
} else {
|
||||
throw new Exception(Exception::MOCK_INVALID_GRANT_TYPE);
|
||||
throw new Exception('Invalid grant type', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -520,7 +520,7 @@ App::get('/v1/mock/tests/general/oauth2/user')
|
|||
->action(function (string $token, Response $response) {
|
||||
|
||||
if ($token != '123456') {
|
||||
throw new Exception(Exception::MOCK_INVALID_TOKEN);
|
||||
throw new Exception('Invalid token', 400, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
$response->json([
|
||||
|
@ -558,24 +558,29 @@ App::get('/v1/mock/tests/general/oauth2/failure')
|
|||
]);
|
||||
});
|
||||
|
||||
App::shutdown(function (App $utopia, Response $response, Request $request) {
|
||||
App::shutdown()
|
||||
->groups(['mock'])
|
||||
->inject('utopia')
|
||||
->inject('response')
|
||||
->inject('request')
|
||||
->action(function (App $utopia, Response $response, Request $request) {
|
||||
|
||||
$result = [];
|
||||
$route = $utopia->match($request);
|
||||
$path = APP_STORAGE_CACHE . '/tests.json';
|
||||
$tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : [];
|
||||
$result = [];
|
||||
$route = $utopia->match($request);
|
||||
$path = APP_STORAGE_CACHE . '/tests.json';
|
||||
$tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : [];
|
||||
|
||||
if (!\is_array($tests)) {
|
||||
throw new Exception(Exception::MOCK_FAILED_TO_READ_RESULTS);
|
||||
}
|
||||
if (!\is_array($tests)) {
|
||||
throw new Exception('Failed to read results', 500, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
$result[$route->getMethod() . ':' . $route->getPath()] = true;
|
||||
$result[$route->getMethod() . ':' . $route->getPath()] = true;
|
||||
|
||||
$tests = \array_merge($tests, $result);
|
||||
$tests = \array_merge($tests, $result);
|
||||
|
||||
if (!\file_put_contents($path, \json_encode($tests), LOCK_EX)) {
|
||||
throw new Exception(Exception::MOCK_FAILED_TO_SAVE_RESULTS);
|
||||
}
|
||||
if (!\file_put_contents($path, \json_encode($tests), LOCK_EX)) {
|
||||
throw new Exception('Failed to save results', 500, Exception::GENERAL_MOCK);
|
||||
}
|
||||
|
||||
$response->dynamic(new Document(['result' => $route->getMethod() . ':' . $route->getPath() . ':passed']), Response::MODEL_MOCK);
|
||||
}, ['utopia', 'response', 'request'], 'mock');
|
||||
$response->dynamic(new Document(['result' => $route->getMethod() . ':' . $route->getPath() . ':passed']), Response::MODEL_MOCK);
|
||||
});
|
||||
|
|
|
@ -19,234 +19,267 @@ use Utopia\Database\Document;
|
|||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Registry\Registry;
|
||||
|
||||
App::init(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Mail $mails, Stats $usage, Delete $deletes, EventDatabase $database, Database $dbForProject, string $mode) {
|
||||
App::init()
|
||||
->groups(['api'])
|
||||
->inject('utopia')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->inject('audits')
|
||||
->inject('mails')
|
||||
->inject('usage')
|
||||
->inject('deletes')
|
||||
->inject('database')
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Mail $mails, Stats $usage, Delete $deletes, EventDatabase $database, Database $dbForProject, string $mode) {
|
||||
|
||||
$route = $utopia->match($request);
|
||||
$route = $utopia->match($request);
|
||||
|
||||
if ($project->isEmpty() && $route->getLabel('abuse-limit', 0) > 0) { // Abuse limit requires an active project scope
|
||||
throw new Exception(Exception::PROJECT_UNKNOWN);
|
||||
}
|
||||
if ($project->isEmpty() && $route->getLabel('abuse-limit', 0) > 0) { // Abuse limit requires an active project scope
|
||||
throw new Exception('Missing or unknown project ID', 400, Exception::PROJECT_UNKNOWN);
|
||||
}
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
*/
|
||||
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
|
||||
$timeLimitArray = [];
|
||||
/*
|
||||
* Abuse Check
|
||||
*/
|
||||
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
|
||||
$timeLimitArray = [];
|
||||
|
||||
$abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
|
||||
$abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
|
||||
|
||||
foreach ($abuseKeyLabel as $abuseKey) {
|
||||
$timeLimit = new TimeLimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForProject);
|
||||
$timeLimit
|
||||
->setParam('{userId}', $user->getId())
|
||||
->setParam('{userAgent}', $request->getUserAgent(''))
|
||||
->setParam('{ip}', $request->getIP())
|
||||
->setParam('{url}', $request->getHostname() . $route->getPath());
|
||||
$timeLimitArray[] = $timeLimit;
|
||||
}
|
||||
foreach ($abuseKeyLabel as $abuseKey) {
|
||||
$timeLimit = new TimeLimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForProject);
|
||||
$timeLimit
|
||||
->setParam('{userId}', $user->getId())
|
||||
->setParam('{userAgent}', $request->getUserAgent(''))
|
||||
->setParam('{ip}', $request->getIP())
|
||||
->setParam('{url}', $request->getHostname() . $route->getPath());
|
||||
$timeLimitArray[] = $timeLimit;
|
||||
}
|
||||
|
||||
$closestLimit = null;
|
||||
$closestLimit = null;
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||
$isAppUser = Auth::isAppUser($roles);
|
||||
$roles = Authorization::getRoles();
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||
$isAppUser = Auth::isAppUser($roles);
|
||||
|
||||
foreach ($timeLimitArray as $timeLimit) {
|
||||
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
|
||||
if (!empty($value)) {
|
||||
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
|
||||
foreach ($timeLimitArray as $timeLimit) {
|
||||
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
|
||||
if (!empty($value)) {
|
||||
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
|
||||
}
|
||||
}
|
||||
|
||||
$abuse = new Abuse($timeLimit);
|
||||
|
||||
if ($timeLimit->limit() && ($timeLimit->remaining() < $closestLimit || is_null($closestLimit))) {
|
||||
$closestLimit = $timeLimit->remaining();
|
||||
$response
|
||||
->addHeader('X-RateLimit-Limit', $timeLimit->limit())
|
||||
->addHeader('X-RateLimit-Remaining', $timeLimit->remaining())
|
||||
->addHeader('X-RateLimit-Reset', $timeLimit->time() + $route->getLabel('abuse-time', 3600))
|
||||
;
|
||||
}
|
||||
|
||||
if (
|
||||
(App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled' // Route is rate-limited
|
||||
&& $abuse->check()) // Abuse is not disabled
|
||||
&& (!$isAppUser && !$isPrivilegedUser)
|
||||
) { // User is not an admin or API key
|
||||
throw new Exception('Too many requests', 429, Exception::GENERAL_RATE_LIMIT_EXCEEDED);
|
||||
}
|
||||
}
|
||||
|
||||
$abuse = new Abuse($timeLimit);
|
||||
|
||||
if ($timeLimit->limit() && ($timeLimit->remaining() < $closestLimit || is_null($closestLimit))) {
|
||||
$closestLimit = $timeLimit->remaining();
|
||||
$response
|
||||
->addHeader('X-RateLimit-Limit', $timeLimit->limit())
|
||||
->addHeader('X-RateLimit-Remaining', $timeLimit->remaining())
|
||||
->addHeader('X-RateLimit-Reset', $timeLimit->time() + $route->getLabel('abuse-time', 3600))
|
||||
;
|
||||
}
|
||||
|
||||
if (
|
||||
(App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled' // Route is rate-limited
|
||||
&& $abuse->check()) // Abuse is not disabled
|
||||
&& (!$isAppUser && !$isPrivilegedUser)
|
||||
) { // User is not an admin or API key
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Background Jobs
|
||||
*/
|
||||
$events
|
||||
->setEvent($route->getLabel('event', ''))
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
;
|
||||
|
||||
$mails
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
;
|
||||
|
||||
$audits
|
||||
->setMode($mode)
|
||||
->setUserAgent($request->getUserAgent(''))
|
||||
->setIP($request->getIP())
|
||||
->setEvent($route->getLabel('event', ''))
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
;
|
||||
|
||||
$usage
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('httpRequest', 1)
|
||||
->setParam('httpUrl', $request->getHostname() . $request->getURI())
|
||||
->setParam('httpMethod', $request->getMethod())
|
||||
->setParam('httpPath', $route->getPath())
|
||||
->setParam('networkRequestSize', 0)
|
||||
->setParam('networkResponseSize', 0)
|
||||
->setParam('storage', 0)
|
||||
;
|
||||
|
||||
$deletes->setProject($project);
|
||||
$database->setProject($project);
|
||||
}, ['utopia', 'request', 'response', 'project', 'user', 'events', 'audits', 'mails', 'usage', 'deletes', 'database', 'dbForProject', 'mode'], 'api');
|
||||
|
||||
App::init(function (App $utopia, Request $request, Document $project) {
|
||||
|
||||
$route = $utopia->match($request);
|
||||
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAppUser = Auth::isAppUser(Authorization::getRoles());
|
||||
|
||||
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
|
||||
return;
|
||||
}
|
||||
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
switch ($route->getLabel('auth.type', '')) {
|
||||
case 'emailPassword':
|
||||
if (($auths['emailPassword'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Email / Password authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'magic-url':
|
||||
if ($project->getAttribute('usersAuthMagicURL', true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Magic URL authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'anonymous':
|
||||
if (($auths['anonymous'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Anonymous authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'invites':
|
||||
if (($auths['invites'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Invites authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jwt':
|
||||
if (($auths['JWT'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'JWT authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route');
|
||||
break;
|
||||
}
|
||||
}, ['utopia', 'request', 'project'], 'auth');
|
||||
|
||||
App::shutdown(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) {
|
||||
|
||||
if (!empty($events->getEvent())) {
|
||||
if (empty($events->getPayload())) {
|
||||
$events->setPayload($response->getPayload());
|
||||
}
|
||||
/**
|
||||
* Trigger functions.
|
||||
*/
|
||||
/*
|
||||
* Background Jobs
|
||||
*/
|
||||
$events
|
||||
->setClass(Event::FUNCTIONS_CLASS_NAME)
|
||||
->setQueue(Event::FUNCTIONS_QUEUE_NAME)
|
||||
->trigger();
|
||||
->setEvent($route->getLabel('event', ''))
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
;
|
||||
|
||||
/**
|
||||
* Trigger webhooks.
|
||||
*/
|
||||
$events
|
||||
->setClass(Event::WEBHOOK_CLASS_NAME)
|
||||
->setQueue(Event::WEBHOOK_QUEUE_NAME)
|
||||
->trigger();
|
||||
$mails
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
;
|
||||
|
||||
/**
|
||||
* Trigger realtime.
|
||||
*/
|
||||
if ($project->getId() !== 'console') {
|
||||
$allEvents = Event::generateEvents($events->getEvent(), $events->getParams());
|
||||
$payload = new Document($events->getPayload());
|
||||
$audits
|
||||
->setMode($mode)
|
||||
->setUserAgent($request->getUserAgent(''))
|
||||
->setIP($request->getIP())
|
||||
->setEvent($route->getLabel('event', ''))
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
;
|
||||
|
||||
$db = $events->getContext('database');
|
||||
$collection = $events->getContext('collection');
|
||||
$bucket = $events->getContext('bucket');
|
||||
|
||||
$target = Realtime::fromPayload(
|
||||
// Pass first, most verbose event pattern
|
||||
event: $allEvents[0],
|
||||
payload: $payload,
|
||||
project: $project,
|
||||
database: $db,
|
||||
collection: $collection,
|
||||
bucket: $bucket,
|
||||
);
|
||||
|
||||
Realtime::send(
|
||||
projectId: $target['projectId'] ?? $project->getId(),
|
||||
payload: $events->getPayload(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles'],
|
||||
options: [
|
||||
'permissionsChanged' => $target['permissionsChanged'],
|
||||
'userId' => $events->getParam('userId')
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($audits->getResource())) {
|
||||
foreach ($events->getParams() as $key => $value) {
|
||||
$audits->setParam($key, $value);
|
||||
}
|
||||
$audits->trigger();
|
||||
}
|
||||
|
||||
if (!empty($deletes->getType())) {
|
||||
$deletes->trigger();
|
||||
}
|
||||
|
||||
if (!empty($database->getType())) {
|
||||
$database->trigger();
|
||||
}
|
||||
|
||||
$route = $utopia->match($request);
|
||||
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();
|
||||
}
|
||||
}, ['utopia', 'request', 'response', 'project', 'events', 'audits', 'usage', 'deletes', 'database', 'mode', 'dbForProject'], 'api');
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('httpRequest', 1)
|
||||
->setParam('httpUrl', $request->getHostname() . $request->getURI())
|
||||
->setParam('httpMethod', $request->getMethod())
|
||||
->setParam('httpPath', $route->getPath())
|
||||
->setParam('networkRequestSize', 0)
|
||||
->setParam('networkResponseSize', 0)
|
||||
->setParam('storage', 0)
|
||||
;
|
||||
|
||||
$deletes->setProject($project);
|
||||
$database->setProject($project);
|
||||
});
|
||||
|
||||
App::init()
|
||||
->groups(['auth'])
|
||||
->inject('utopia')
|
||||
->inject('request')
|
||||
->inject('project')
|
||||
->action(function (App $utopia, Request $request, Document $project) {
|
||||
|
||||
$route = $utopia->match($request);
|
||||
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAppUser = Auth::isAppUser(Authorization::getRoles());
|
||||
|
||||
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
|
||||
return;
|
||||
}
|
||||
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
switch ($route->getLabel('auth.type', '')) {
|
||||
case 'emailPassword':
|
||||
if (($auths['emailPassword'] ?? true) === false) {
|
||||
throw new Exception('Email / Password authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'magic-url':
|
||||
if ($project->getAttribute('usersAuthMagicURL', true) === false) {
|
||||
throw new Exception('Magic URL authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'anonymous':
|
||||
if (($auths['anonymous'] ?? true) === false) {
|
||||
throw new Exception('Anonymous authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'invites':
|
||||
if (($auths['invites'] ?? true) === false) {
|
||||
throw new Exception('Invites authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jwt':
|
||||
if (($auths['JWT'] ?? true) === false) {
|
||||
throw new Exception('JWT authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('Unsupported authentication route', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
App::shutdown()
|
||||
->groups(['api'])
|
||||
->inject('utopia')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('events')
|
||||
->inject('audits')
|
||||
->inject('usage')
|
||||
->inject('deletes')
|
||||
->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) {
|
||||
|
||||
if (!empty($events->getEvent())) {
|
||||
if (empty($events->getPayload())) {
|
||||
$events->setPayload($response->getPayload());
|
||||
}
|
||||
/**
|
||||
* Trigger functions.
|
||||
*/
|
||||
$events
|
||||
->setClass(Event::FUNCTIONS_CLASS_NAME)
|
||||
->setQueue(Event::FUNCTIONS_QUEUE_NAME)
|
||||
->trigger();
|
||||
|
||||
/**
|
||||
* Trigger webhooks.
|
||||
*/
|
||||
$events
|
||||
->setClass(Event::WEBHOOK_CLASS_NAME)
|
||||
->setQueue(Event::WEBHOOK_QUEUE_NAME)
|
||||
->trigger();
|
||||
|
||||
/**
|
||||
* Trigger realtime.
|
||||
*/
|
||||
if ($project->getId() !== 'console') {
|
||||
$allEvents = Event::generateEvents($events->getEvent(), $events->getParams());
|
||||
$payload = new Document($events->getPayload());
|
||||
|
||||
$db = $events->getContext('database');
|
||||
$collection = $events->getContext('collection');
|
||||
$bucket = $events->getContext('bucket');
|
||||
|
||||
$target = Realtime::fromPayload(
|
||||
// Pass first, most verbose event pattern
|
||||
event: $allEvents[0],
|
||||
payload: $payload,
|
||||
project: $project,
|
||||
database: $db,
|
||||
collection: $collection,
|
||||
bucket: $bucket,
|
||||
);
|
||||
|
||||
Realtime::send(
|
||||
projectId: $target['projectId'] ?? $project->getId(),
|
||||
payload: $events->getPayload(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles'],
|
||||
options: [
|
||||
'permissionsChanged' => $target['permissionsChanged'],
|
||||
'userId' => $events->getParam('userId')
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($audits->getResource())) {
|
||||
foreach ($events->getParams() as $key => $value) {
|
||||
$audits->setParam($key, $value);
|
||||
}
|
||||
$audits->trigger();
|
||||
}
|
||||
|
||||
if (!empty($deletes->getType())) {
|
||||
$deletes->trigger();
|
||||
}
|
||||
|
||||
if (!empty($database->getType())) {
|
||||
$database->trigger();
|
||||
}
|
||||
|
||||
$route = $utopia->match($request);
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -6,54 +6,59 @@ use Appwrite\Utopia\Response;
|
|||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\View;
|
||||
|
||||
App::init(function (App $utopia, Request $request, Response $response, View $layout) {
|
||||
App::init()
|
||||
->groups(['web'])
|
||||
->inject('utopia')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('layout')
|
||||
->action(function (App $utopia, Request $request, Response $response, View $layout) {
|
||||
/* AJAX check */
|
||||
if (!empty($request->getQuery('version', ''))) {
|
||||
$layout->setPath(__DIR__ . '/../../views/layouts/empty.phtml');
|
||||
}
|
||||
|
||||
/* AJAX check */
|
||||
if (!empty($request->getQuery('version', ''))) {
|
||||
$layout->setPath(__DIR__ . '/../../views/layouts/empty.phtml');
|
||||
}
|
||||
$port = $request->getPort();
|
||||
$protocol = $request->getProtocol();
|
||||
$domain = $request->getHostname();
|
||||
|
||||
$port = $request->getPort();
|
||||
$protocol = $request->getProtocol();
|
||||
$domain = $request->getHostname();
|
||||
$layout
|
||||
->setParam('title', APP_NAME)
|
||||
->setParam('protocol', $protocol)
|
||||
->setParam('domain', $domain)
|
||||
->setParam('endpoint', $protocol . '://' . $domain . ($port != 80 && $port != 443 ? ':' . $port : ''))
|
||||
->setParam('home', App::getEnv('_APP_HOME'))
|
||||
->setParam('setup', App::getEnv('_APP_SETUP'))
|
||||
->setParam('class', 'unknown')
|
||||
->setParam('icon', '/images/favicon.png')
|
||||
->setParam('roles', [
|
||||
['type' => 'owner', 'label' => 'Owner'],
|
||||
['type' => 'developer', 'label' => 'Developer'],
|
||||
['type' => 'admin', 'label' => 'Admin'],
|
||||
])
|
||||
->setParam('runtimes', Config::getParam('runtimes'))
|
||||
->setParam('mode', App::getMode())
|
||||
;
|
||||
|
||||
$layout
|
||||
->setParam('title', APP_NAME)
|
||||
->setParam('protocol', $protocol)
|
||||
->setParam('domain', $domain)
|
||||
->setParam('endpoint', $protocol . '://' . $domain . ($port != 80 && $port != 443 ? ':' . $port : ''))
|
||||
->setParam('home', App::getEnv('_APP_HOME'))
|
||||
->setParam('setup', App::getEnv('_APP_SETUP'))
|
||||
->setParam('class', 'unknown')
|
||||
->setParam('icon', '/images/favicon.png')
|
||||
->setParam('roles', [
|
||||
['type' => 'owner', 'label' => 'Owner'],
|
||||
['type' => 'developer', 'label' => 'Developer'],
|
||||
['type' => 'admin', 'label' => 'Admin'],
|
||||
])
|
||||
->setParam('runtimes', Config::getParam('runtimes'))
|
||||
->setParam('mode', App::getMode())
|
||||
;
|
||||
$time = (60 * 60 * 24 * 45); // 45 days cache
|
||||
|
||||
$time = (60 * 60 * 24 * 45); // 45 days cache
|
||||
$response
|
||||
->addHeader('Cache-Control', 'public, max-age=' . $time)
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache
|
||||
->addHeader('X-Frame-Options', 'SAMEORIGIN') // Avoid console and homepage from showing in iframes
|
||||
->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url=' . \urlencode($request->getURI()))
|
||||
->addHeader('X-UA-Compatible', 'IE=Edge') // Deny IE browsers from going into quirks mode
|
||||
;
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'public, max-age=' . $time)
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache
|
||||
->addHeader('X-Frame-Options', 'SAMEORIGIN') // Avoid console and homepage from showing in iframes
|
||||
->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url=' . \urlencode($request->getURI()))
|
||||
->addHeader('X-UA-Compatible', 'IE=Edge') // Deny IE browsers from going into quirks mode
|
||||
;
|
||||
$route = $utopia->match($request);
|
||||
|
||||
$route = $utopia->match($request);
|
||||
$route->label('error', __DIR__ . '/../../views/general/error.phtml');
|
||||
|
||||
$route->label('error', __DIR__ . '/../../views/general/error.phtml');
|
||||
$scope = $route->getLabel('scope', '');
|
||||
|
||||
$scope = $route->getLabel('scope', '');
|
||||
|
||||
$layout
|
||||
->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN'))
|
||||
->setParam('isDev', App::isDevelopment())
|
||||
->setParam('class', $scope)
|
||||
;
|
||||
}, ['utopia', 'request', 'response', 'layout'], 'web');
|
||||
$layout
|
||||
->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN'))
|
||||
->setParam('isDev', App::isDevelopment())
|
||||
->setParam('class', $scope)
|
||||
;
|
||||
});
|
||||
|
|
|
@ -9,31 +9,36 @@ use Utopia\Domains\Domain;
|
|||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Storage\Storage;
|
||||
|
||||
App::init(function (View $layout) {
|
||||
App::init()
|
||||
->groups(['console'])
|
||||
->inject('layout')
|
||||
->action(function (View $layout) {
|
||||
$layout
|
||||
->setParam('description', 'Appwrite Console allows you to easily manage, monitor, and control your entire backend API and tools.')
|
||||
->setParam('analytics', 'UA-26264668-5')
|
||||
;
|
||||
});
|
||||
|
||||
$layout
|
||||
->setParam('description', 'Appwrite Console allows you to easily manage, monitor, and control your entire backend API and tools.')
|
||||
->setParam('analytics', 'UA-26264668-5')
|
||||
;
|
||||
}, ['layout'], 'console');
|
||||
App::shutdown()
|
||||
->groups(['console'])
|
||||
->inject('response')
|
||||
->inject('layout')
|
||||
->action(function (Response $response, View $layout) {
|
||||
$header = new View(__DIR__ . '/../../views/console/comps/header.phtml');
|
||||
$footer = new View(__DIR__ . '/../../views/console/comps/footer.phtml');
|
||||
|
||||
App::shutdown(function (Response $response, View $layout) {
|
||||
$footer
|
||||
->setParam('home', App::getEnv('_APP_HOME', ''))
|
||||
->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN'))
|
||||
;
|
||||
|
||||
$header = new View(__DIR__ . '/../../views/console/comps/header.phtml');
|
||||
$footer = new View(__DIR__ . '/../../views/console/comps/footer.phtml');
|
||||
$layout
|
||||
->setParam('header', [$header])
|
||||
->setParam('footer', [$footer])
|
||||
;
|
||||
|
||||
$footer
|
||||
->setParam('home', App::getEnv('_APP_HOME', ''))
|
||||
->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN'))
|
||||
;
|
||||
|
||||
$layout
|
||||
->setParam('header', [$header])
|
||||
->setParam('footer', [$footer])
|
||||
;
|
||||
|
||||
$response->html($layout->render());
|
||||
}, ['response', 'layout'], 'console');
|
||||
$response->html($layout->render());
|
||||
});
|
||||
|
||||
App::get('/error/:code')
|
||||
->groups(['web', 'console'])
|
||||
|
|
|
@ -7,29 +7,34 @@ use Utopia\Config\Config;
|
|||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
App::init(function (View $layout) {
|
||||
App::init()
|
||||
->groups(['home'])
|
||||
->inject('layout')
|
||||
->action(function (View $layout) {
|
||||
$header = new View(__DIR__ . '/../../views/home/comps/header.phtml');
|
||||
$footer = new View(__DIR__ . '/../../views/home/comps/footer.phtml');
|
||||
|
||||
$header = new View(__DIR__ . '/../../views/home/comps/header.phtml');
|
||||
$footer = new View(__DIR__ . '/../../views/home/comps/footer.phtml');
|
||||
$footer
|
||||
->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN'))
|
||||
;
|
||||
|
||||
$footer
|
||||
->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN'))
|
||||
;
|
||||
$layout
|
||||
->setParam('title', APP_NAME)
|
||||
->setParam('description', '')
|
||||
->setParam('class', 'home')
|
||||
->setParam('platforms', Config::getParam('platforms'))
|
||||
->setParam('header', [$header])
|
||||
->setParam('footer', [$footer])
|
||||
;
|
||||
});
|
||||
|
||||
$layout
|
||||
->setParam('title', APP_NAME)
|
||||
->setParam('description', '')
|
||||
->setParam('class', 'home')
|
||||
->setParam('platforms', Config::getParam('platforms'))
|
||||
->setParam('header', [$header])
|
||||
->setParam('footer', [$footer])
|
||||
;
|
||||
}, ['layout'], 'home');
|
||||
|
||||
App::shutdown(function (Response $response, View $layout) {
|
||||
|
||||
$response->html($layout->render());
|
||||
}, ['response', 'layout'], 'home');
|
||||
App::shutdown()
|
||||
->groups(['home'])
|
||||
->inject('response')
|
||||
->inject('layout')
|
||||
->action(function (Response $response, View $layout) {
|
||||
$response->html($layout->render());
|
||||
});
|
||||
|
||||
App::get('/')
|
||||
->groups(['web', 'home'])
|
||||
|
|
|
@ -581,57 +581,64 @@ App::setResource('orchestrationPool', fn() => $orchestrationPool);
|
|||
App::setResource('activeRuntimes', fn() => $activeRuntimes);
|
||||
|
||||
/** Set callbacks */
|
||||
App::error(function ($utopia, $error, $request, $response) {
|
||||
$route = $utopia->match($request);
|
||||
logError($error, "httpError", $route);
|
||||
App::error()
|
||||
->inject('utopia')
|
||||
->inject('error')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->action(function (App $utopia, throwable $error, Request $request, Response $response) {
|
||||
$route = $utopia->match($request);
|
||||
logError($error, "httpError", $route);
|
||||
|
||||
switch ($error->getCode()) {
|
||||
case 400: // Error allowed publicly
|
||||
case 401: // Error allowed publicly
|
||||
case 402: // Error allowed publicly
|
||||
case 403: // Error allowed publicly
|
||||
case 404: // Error allowed publicly
|
||||
case 406: // Error allowed publicly
|
||||
case 409: // Error allowed publicly
|
||||
case 412: // Error allowed publicly
|
||||
case 425: // Error allowed publicly
|
||||
case 429: // Error allowed publicly
|
||||
case 501: // Error allowed publicly
|
||||
case 503: // Error allowed publicly
|
||||
$code = $error->getCode();
|
||||
break;
|
||||
default:
|
||||
$code = 500; // All other errors get the generic 500 server error status code
|
||||
}
|
||||
switch ($error->getCode()) {
|
||||
case 400: // Error allowed publicly
|
||||
case 401: // Error allowed publicly
|
||||
case 402: // Error allowed publicly
|
||||
case 403: // Error allowed publicly
|
||||
case 404: // Error allowed publicly
|
||||
case 406: // Error allowed publicly
|
||||
case 409: // Error allowed publicly
|
||||
case 412: // Error allowed publicly
|
||||
case 425: // Error allowed publicly
|
||||
case 429: // Error allowed publicly
|
||||
case 501: // Error allowed publicly
|
||||
case 503: // Error allowed publicly
|
||||
$code = $error->getCode();
|
||||
break;
|
||||
default:
|
||||
$code = 500; // All other errors get the generic 500 server error status code
|
||||
}
|
||||
|
||||
$output = [
|
||||
'message' => $error->getMessage(),
|
||||
'code' => $error->getCode(),
|
||||
'file' => $error->getFile(),
|
||||
'line' => $error->getLine(),
|
||||
'trace' => $error->getTrace(),
|
||||
'version' => App::getEnv('_APP_VERSION', 'UNKNOWN')
|
||||
];
|
||||
$output = [
|
||||
'message' => $error->getMessage(),
|
||||
'code' => $error->getCode(),
|
||||
'file' => $error->getFile(),
|
||||
'line' => $error->getLine(),
|
||||
'trace' => $error->getTrace(),
|
||||
'version' => App::getEnv('_APP_VERSION', 'UNKNOWN')
|
||||
];
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Expires', '0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->setStatusCode($code);
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Expires', '0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->setStatusCode($code);
|
||||
|
||||
$response->json($output);
|
||||
}, ['utopia', 'error', 'request', 'response']);
|
||||
$response->json($output);
|
||||
});
|
||||
|
||||
App::init(function ($request, $response) {
|
||||
$secretKey = $request->getHeader('x-appwrite-executor-key', '');
|
||||
if (empty($secretKey)) {
|
||||
throw new Exception('Missing executor key', 401);
|
||||
}
|
||||
App::init()
|
||||
->inject('request')
|
||||
->action(function (Request $request) {
|
||||
$secretKey = $request->getHeader('x-appwrite-executor-key', '');
|
||||
if (empty($secretKey)) {
|
||||
throw new Exception('Missing executor key', 401);
|
||||
}
|
||||
|
||||
if ($secretKey !== App::getEnv('_APP_EXECUTOR_SECRET', '')) {
|
||||
throw new Exception('Missing executor key', 401);
|
||||
}
|
||||
}, ['request', 'response']);
|
||||
if ($secretKey !== App::getEnv('_APP_EXECUTOR_SECRET', '')) {
|
||||
throw new Exception('Missing executor key', 401);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$http->on('start', function ($http) {
|
||||
|
|
|
@ -89,7 +89,7 @@ const APP_LIMIT_ARRAY_PARAMS_SIZE = 100; // Default maximum of how many elements
|
|||
const APP_LIMIT_ARRAY_ELEMENT_SIZE = 4096; // Default maximum length of element in array parameter represented by maximum URL length.
|
||||
const APP_LIMIT_SUBQUERY = 1000;
|
||||
const APP_CACHE_BUSTER = 402;
|
||||
const APP_VERSION_STABLE = '0.15.2';
|
||||
const APP_VERSION_STABLE = '0.15.3';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
|
|
12
app/views/console/users/oauth/authentik.phtml
Normal file
12
app/views/console/users/oauth/authentik.phtml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
$provider = $this->getParam('provider', '');
|
||||
?>
|
||||
|
||||
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid">Client ID<span class="tooltip" data-tooltip="Provided in the Provider you created in authentik"><i class="icon-info-circled"></i></span></label>
|
||||
<input name="appId" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid" type="text" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Appid}}" placeholder="Client ID" />
|
||||
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>ClientSecret">Client Secret <span class="tooltip" data-tooltip="Provided in the Provider you created in authentik"><i class="icon-info-circled"></i></span></label>
|
||||
<input name="clientSecret" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>ClientSecret" type="password" autocomplete="off" placeholder="Client Secret" />
|
||||
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Domain">authentik Base-Domain<span class="tooltip" data-tooltip="Your authentik Base-Domain (without 'https://')"><i class="icon-info-circled"></i></span></label>
|
||||
<input name="authentikDomain" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Domain" type="text" autocomplete="off" placeholder="auth.example.com" />
|
||||
<?php /*Hidden input for the final secret. Gets filled with a JSON via JS. */ ?>
|
||||
<input name="secret" data-forms-oauth-custom="<?php echo $this->escape(ucfirst($provider)); ?>" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret" type="hidden" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Secret}}" />
|
|
@ -42,7 +42,7 @@
|
|||
"ext-sockets": "*",
|
||||
"appwrite/php-clamav": "1.1.*",
|
||||
"appwrite/php-runtimes": "0.10.*",
|
||||
"utopia-php/framework": "0.19.*",
|
||||
"utopia-php/framework": "0.20.*",
|
||||
"utopia-php/logger": "0.3.*",
|
||||
"utopia-php/abuse": "0.7.*",
|
||||
"utopia-php/analytics": "0.2.*",
|
||||
|
|
53
composer.lock
generated
53
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "380d806f7540199698d12a7abeb13534",
|
||||
"content-hash": "0a8ed4fa28bf33ceb7396c35b9e8a155",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
|
@ -2051,16 +2051,16 @@
|
|||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "0.18.7",
|
||||
"version": "0.18.9",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "d542ee433f1a545d926ffaf707bdf952dc18a52e"
|
||||
"reference": "227b3ca919149b7b0d6556c8effe9ee46ed081e6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/d542ee433f1a545d926ffaf707bdf952dc18a52e",
|
||||
"reference": "d542ee433f1a545d926ffaf707bdf952dc18a52e",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/227b3ca919149b7b0d6556c8effe9ee46ed081e6",
|
||||
"reference": "227b3ca919149b7b0d6556c8effe9ee46ed081e6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2109,9 +2109,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/0.18.7"
|
||||
"source": "https://github.com/utopia-php/database/tree/0.18.9"
|
||||
},
|
||||
"time": "2022-07-11T10:20:33+00:00"
|
||||
"time": "2022-07-19T09:42:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/domains",
|
||||
|
@ -2169,16 +2169,16 @@
|
|||
},
|
||||
{
|
||||
"name": "utopia-php/framework",
|
||||
"version": "0.19.21",
|
||||
"version": "0.20.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/framework.git",
|
||||
"reference": "3b7bd8e4acf84fd7d560ced8e0142221d302575d"
|
||||
"reference": "beb5e861c7d0a6256a1272e6b9d70b060ca8629a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/framework/zipball/3b7bd8e4acf84fd7d560ced8e0142221d302575d",
|
||||
"reference": "3b7bd8e4acf84fd7d560ced8e0142221d302575d",
|
||||
"url": "https://api.github.com/repos/utopia-php/framework/zipball/beb5e861c7d0a6256a1272e6b9d70b060ca8629a",
|
||||
"reference": "beb5e861c7d0a6256a1272e6b9d70b060ca8629a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2212,9 +2212,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/framework/issues",
|
||||
"source": "https://github.com/utopia-php/framework/tree/0.19.21"
|
||||
"source": "https://github.com/utopia-php/framework/tree/0.20.0"
|
||||
},
|
||||
"time": "2022-05-12T18:42:28+00:00"
|
||||
"time": "2022-07-30T09:55:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
|
@ -2387,16 +2387,16 @@
|
|||
},
|
||||
{
|
||||
"name": "utopia-php/orchestration",
|
||||
"version": "dev-cli-lib-upgrade",
|
||||
"version": "0.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/orchestration.git",
|
||||
"reference": "06f2afef516aca900ddb483689ebe6f8e7037d28"
|
||||
"reference": "94263976413871efb6b16157a7101a81df3b6d78"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/orchestration/zipball/06f2afef516aca900ddb483689ebe6f8e7037d28",
|
||||
"reference": "06f2afef516aca900ddb483689ebe6f8e7037d28",
|
||||
"url": "https://api.github.com/repos/utopia-php/orchestration/zipball/94263976413871efb6b16157a7101a81df3b6d78",
|
||||
"reference": "94263976413871efb6b16157a7101a81df3b6d78",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2436,9 +2436,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/orchestration/issues",
|
||||
"source": "https://github.com/utopia-php/orchestration/tree/cli-lib-upgrade"
|
||||
"source": "https://github.com/utopia-php/orchestration/tree/0.6.0"
|
||||
},
|
||||
"time": "2022-07-13T14:55:12+00:00"
|
||||
"time": "2022-07-13T16:47:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/preloader",
|
||||
|
@ -5346,18 +5346,9 @@
|
|||
"time": "2022-05-17T05:48:52+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
{
|
||||
"package": "utopia-php/orchestration",
|
||||
"version": "dev-cli-lib-upgrade",
|
||||
"alias": "0.4.1",
|
||||
"alias_normalized": "0.4.1.0"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {
|
||||
"utopia-php/orchestration": 20
|
||||
},
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
|
@ -5379,5 +5370,5 @@
|
|||
"platform-overrides": {
|
||||
"php": "8.0"
|
||||
},
|
||||
"plugin-api-version": "2.3.0"
|
||||
"plugin-api-version": "2.2.0"
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Create a new Collection. Before using this route, you should create a new database resource using either a [server integration](/docs/server/database#databaseCreateCollection) API or directly from your database console.
|
||||
Create a new Collection. Before using this route, you should create a new database resource using either a [server integration](/docs/server/databases#databasesCreateCollection) API or directly from your database console.
|
|
@ -1 +1 @@
|
|||
Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](/docs/server/database#databaseCreateCollection) API or directly from your database console.
|
||||
Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](/docs/server/databases#databasesCreateCollection) API or directly from your database console.
|
|
@ -1,4 +1,4 @@
|
|||
Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](/docs/server/database#storageCreateBucket) API or directly from your Appwrite console.
|
||||
Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](/docs/server/storage#storageCreateBucket) API or directly from your Appwrite console.
|
||||
|
||||
Larger files should be uploaded using multiple requests with the [content-range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.
|
||||
|
||||
|
|
|
@ -2,6 +2,6 @@ The Databases service allows you to create structured collections of documents,
|
|||
|
||||
All data returned by the Databases service are represented as structured JSON documents.
|
||||
|
||||
The Databases service can contain multiple databases, each database can contain multiple collections. A collection is a group of similarly structured documents. The accepted structure of documents is defined by [collection attributes](/docs/database#attributes). The collection attributes help you ensure all your user-submitted data is validated and stored according to the collection structure.
|
||||
The Databases service can contain multiple databases, each database can contain multiple collections. A collection is a group of similarly structured documents. The accepted structure of documents is defined by [collection attributes](/docs/databases#attributes). The collection attributes help you ensure all your user-submitted data is validated and stored according to the collection structure.
|
||||
|
||||
Using Appwrite permissions architecture, you can assign read or write access to each collection or document in your project for either a specific user, team, user role, or even grant it with public access (`role:all`). You can learn more about [how Appwrite handles permissions and access control](/docs/permissions).
|
446
public/dist/scripts/app-all.js
vendored
446
public/dist/scripts/app-all.js
vendored
File diff suppressed because one or more lines are too long
444
public/dist/scripts/app-dep.js
vendored
444
public/dist/scripts/app-dep.js
vendored
File diff suppressed because one or more lines are too long
2
public/dist/scripts/app.js
vendored
2
public/dist/scripts/app.js
vendored
|
@ -797,7 +797,7 @@ list["filters-"+filter.key]=params[key][i];}}}}
|
|||
return list;};let apply=function(params){let cached=container.get(name);cached=cached?cached.params:[];params=Object.assign(cached,params);container.set(name,{name:name,params:params,query:serialize(params),forward:parseInt(params.offset)+parseInt(params.limit),backward:parseInt(params.offset)-parseInt(params.limit),keys:flatten(params)},true,name);document.dispatchEvent(new CustomEvent(name+"-changed",{bubbles:false,cancelable:true}));};switch(element.tagName){case"INPUT":break;case"TEXTAREA":break;case"BUTTON":element.addEventListener("click",function(){apply(JSON.parse(expression.parse(element.dataset["params"]||"{}")));});break;case"FORM":element.addEventListener("input",function(){apply(form.toJson(element));});element.addEventListener("change",function(){apply(form.toJson(element));});element.addEventListener("reset",function(){setTimeout(function(){apply(form.toJson(element));},0);});events=events.trim().split(",");for(let y=0;y<events.length;y++){if(events[y]==="init"){element.addEventListener("rendered",function(){apply(form.toJson(element));},{once:true});}else{}
|
||||
element.setAttribute("data-event","none");}
|
||||
break;default:break;}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-forms-headers",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";value.type="text";value.className="margin-bottom-no";value.placeholder="Value";wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.value=key.value.toLowerCase()+":"+value.value.toLowerCase();};let syncB=function(){let split=element.value.toLowerCase().split(":");key.value=split[0]||"";value.value=split[1]||"";key.value=key.value.trim();value.value=value.value.trim();};syncB();}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-forms-key-value",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";key.required=true;value.type="text";value.className="margin-bottom-no";value.placeholder="Value";value.required=true;wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.name=key.value;element.value=value.value;};let syncB=function(){key.value=element.name||"";value.value=element.value||"";};syncB();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-move-down",controller:function(element){Array.prototype.slice.call(element.querySelectorAll("[data-move-down]")).map(function(obj){obj.addEventListener("click",function(){if(element.nextElementSibling){console.log('down',element.offsetHeight);element.parentNode.insertBefore(element.nextElementSibling,element);element.scrollIntoView({block:'center'});}});});}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-move-up",controller:function(element){Array.prototype.slice.call(element.querySelectorAll("[data-move-up]")).map(function(obj){obj.addEventListener("click",function(){if(element.previousElementSibling){console.log('up',element);element.parentNode.insertBefore(element,element.previousElementSibling);element.scrollIntoView({block:'center'});}});});}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-nav",repeat:false,controller:function(element,view,container,document){let titles=document.querySelectorAll('[data-forms-nav-anchor]');let links=element.querySelectorAll('[data-forms-nav-link]');let minLink=null;let check=function(){let minDistance=null;let minElement=null;for(let i=0;i<titles.length;++i){let title=titles[i];let distance=title.getBoundingClientRect().top;console.log(i);if((minDistance===null||minDistance>=distance)&&(distance>=0)){if(minLink){minLink.classList.remove('selected');}
|
||||
console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-custom",controller:function(element){let providers={"Microsoft":{"clientSecret":"oauth2MicrosoftClientSecret","tenantID":"oauth2MicrosoftTenantId"},"Apple":{"keyID":"oauth2AppleKeyId","teamID":"oauth2AppleTeamId","p8":"oauth2AppleP8"},"Okta":{"clientSecret":"oauth2OktaClientSecret","oktaDomain":"oauth2OktaDomain","authorizationServerId":"oauth2OktaAuthorizationServerId"},"Auth0":{"clientSecret":"oauth2Auth0ClientSecret","auth0Domain":"oauth2Auth0Domain"},"Gitlab":{"endpoint":"oauth2GitlabEndpoint","clientSecret":"oauth2GitlabClientSecret",},}
|
||||
console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-custom",controller:function(element){let providers={"Microsoft":{"clientSecret":"oauth2MicrosoftClientSecret","tenantID":"oauth2MicrosoftTenantId"},"Apple":{"keyID":"oauth2AppleKeyId","teamID":"oauth2AppleTeamId","p8":"oauth2AppleP8"},"Okta":{"clientSecret":"oauth2OktaClientSecret","oktaDomain":"oauth2OktaDomain","authorizationServerId":"oauth2OktaAuthorizationServerId"},"Auth0":{"clientSecret":"oauth2Auth0ClientSecret","auth0Domain":"oauth2Auth0Domain"},"Authentik":{"clientSecret":"oauth2AuthentikClientSecret","authentikDomain":"oauth2AuthentikDomain"},"Gitlab":{"endpoint":"oauth2GitlabEndpoint","clientSecret":"oauth2GitlabClientSecret",},}
|
||||
let provider=element.getAttribute("data-forms-oauth-custom");if(!provider||!providers.hasOwnProperty(provider)){console.error("Provider for custom form not set or unknown")}
|
||||
let config=providers[provider];element.addEventListener('change',sync);let elements={};for(const key in config){if(Object.hasOwnProperty.call(config,key)){elements[key]=document.getElementById(config[key]);elements[key].addEventListener('change',update);}}
|
||||
function update(){let json={};for(const key in elements){if(Object.hasOwnProperty.call(elements,key)){json[key]=elements[key].value}}
|
||||
|
|
2
public/dist/styles/default-ltr.css
vendored
2
public/dist/styles/default-ltr.css
vendored
File diff suppressed because one or more lines are too long
2
public/dist/styles/default-rtl.css
vendored
2
public/dist/styles/default-rtl.css
vendored
File diff suppressed because one or more lines are too long
BIN
public/images/users/authentik.png
Normal file
BIN
public/images/users/authentik.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 827 B |
BIN
public/images/users/podio.png
Normal file
BIN
public/images/users/podio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
|
@ -2227,7 +2227,7 @@
|
|||
*
|
||||
* Create a new Document. Before using this route, you should create a new
|
||||
* collection resource using either a [server
|
||||
* integration](/docs/server/database#databaseCreateCollection) API or
|
||||
* integration](/docs/server/databases#databasesCreateCollection) API or
|
||||
* directly from your database console.
|
||||
*
|
||||
* @param {string} databaseId
|
||||
|
@ -4745,7 +4745,7 @@
|
|||
*
|
||||
* Create a new file. Before using this route, you should create a new bucket
|
||||
* resource using either a [server
|
||||
* integration](/docs/server/database#storageCreateBucket) API or directly
|
||||
* integration](/docs/server/storage#storageCreateBucket) API or directly
|
||||
* from your Appwrite console.
|
||||
*
|
||||
* Larger files should be uploaded using multiple requests with the
|
||||
|
|
|
@ -26,6 +26,10 @@
|
|||
"clientSecret": "oauth2Auth0ClientSecret",
|
||||
"auth0Domain": "oauth2Auth0Domain"
|
||||
},
|
||||
"Authentik": {
|
||||
"clientSecret": "oauth2AuthentikClientSecret",
|
||||
"authentikDomain": "oauth2AuthentikDomain"
|
||||
},
|
||||
"Gitlab": {
|
||||
"endpoint": "oauth2GitlabEndpoint",
|
||||
"clientSecret": "oauth2GitlabClientSecret",
|
||||
|
|
|
@ -41,14 +41,14 @@
|
|||
i {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
top: 9px;
|
||||
.func-start(9px);
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
top: 8px;
|
||||
.func-start(8px);
|
||||
color: var(--config-color-background-dark);
|
||||
background: var(--config-color-normal);
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
|
227
src/Appwrite/Auth/OAuth2/Authentik.php
Normal file
227
src/Appwrite/Auth/OAuth2/Authentik.php
Normal file
|
@ -0,0 +1,227 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Auth\OAuth2;
|
||||
|
||||
use Appwrite\Auth\OAuth2;
|
||||
|
||||
// Reference Material
|
||||
// https://goauthentik.io/docs/providers/oauth2/
|
||||
|
||||
class Authentik extends OAuth2
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $scopes = [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'offline_access'
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $user = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $tokens = [];
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'authentik';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLoginURL(): string
|
||||
{
|
||||
return 'https://' . $this->getAuthentikDomain() . '/application/o/authorize?' . \http_build_query([
|
||||
'client_id' => $this->appID,
|
||||
'redirect_uri' => $this->callback,
|
||||
'state' => \json_encode($this->state),
|
||||
'scope' => \implode(' ', $this->getScopes()),
|
||||
'response_type' => 'code'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getTokens(string $code): array
|
||||
{
|
||||
if (empty($this->tokens)) {
|
||||
$headers = ['Content-Type: application/x-www-form-urlencoded'];
|
||||
$this->tokens = \json_decode($this->request(
|
||||
'POST',
|
||||
'https://' . $this->getAuthentikDomain() . '/application/o/token/',
|
||||
$headers,
|
||||
\http_build_query([
|
||||
'code' => $code,
|
||||
'client_id' => $this->appID,
|
||||
'client_secret' => $this->getClientSecret(),
|
||||
'redirect_uri' => $this->callback,
|
||||
'scope' => \implode(' ', $this->getScopes()),
|
||||
'grant_type' => 'authorization_code'
|
||||
])
|
||||
), true);
|
||||
}
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $refreshToken
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function refreshTokens(string $refreshToken): array
|
||||
{
|
||||
$headers = ['Content-Type: application/x-www-form-urlencoded'];
|
||||
$this->tokens = \json_decode($this->request(
|
||||
'POST',
|
||||
'https://' . $this->getAuthentikDomain() . '/application/o/token/',
|
||||
$headers,
|
||||
\http_build_query([
|
||||
'refresh_token' => $refreshToken,
|
||||
'client_id' => $this->appID,
|
||||
'client_secret' => $this->getClientSecret(),
|
||||
'grant_type' => 'refresh_token'
|
||||
])
|
||||
), true);
|
||||
|
||||
if (empty($this->tokens['refresh_token'])) {
|
||||
$this->tokens['refresh_token'] = $refreshToken;
|
||||
}
|
||||
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserID(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
if (isset($user['sub'])) {
|
||||
return $user['sub'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserEmail(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
if (isset($user['email'])) {
|
||||
return $user['email'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the User email is verified
|
||||
*
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmailVerified(string $accessToken): bool
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
if ($user['email_verified'] ?? false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserName(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
if (isset($user['name'])) {
|
||||
return $user['name'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getUser(string $accessToken): array
|
||||
{
|
||||
if (empty($this->user)) {
|
||||
$headers = ['Authorization: Bearer ' . \urlencode($accessToken)];
|
||||
$user = $this->request('GET', 'https://' . $this->getAuthentikDomain() . '/application/o/userinfo/', $headers);
|
||||
$this->user = \json_decode($user, true);
|
||||
}
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the Client Secret from the JSON stored in appSecret
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getClientSecret(): string
|
||||
{
|
||||
$secret = $this->getAppSecret();
|
||||
|
||||
return $secret['clientSecret'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the authentik Domain from the JSON stored in appSecret
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getAuthentikDomain(): string
|
||||
{
|
||||
$secret = $this->getAppSecret();
|
||||
return $secret['authentikDomain'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON stored in appSecret
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getAppSecret(): array
|
||||
{
|
||||
try {
|
||||
$secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\Throwable $th) {
|
||||
throw new \Exception('Invalid secret');
|
||||
}
|
||||
return $secret;
|
||||
}
|
||||
}
|
199
src/Appwrite/Auth/OAuth2/Podio.php
Normal file
199
src/Appwrite/Auth/OAuth2/Podio.php
Normal file
|
@ -0,0 +1,199 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Auth\OAuth2;
|
||||
|
||||
use Appwrite\Auth\OAuth2;
|
||||
|
||||
// Reference Material
|
||||
// https://developers.podio.com/doc/oauth-authorization
|
||||
|
||||
class Podio extends OAuth2
|
||||
{
|
||||
/**
|
||||
* Endpoint used for initiating OAuth flow
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $endpoint = 'https://podio.com/oauth';
|
||||
|
||||
/**
|
||||
* Endpoint for communication with API server
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $apiEndpoint = 'https://api.podio.com';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $user = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $tokens = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $scopes = []; // No scopes required
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'podio';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLoginURL(): string
|
||||
{
|
||||
$url = $this->endpoint . '/authorize?' .
|
||||
\http_build_query([
|
||||
'client_id' => $this->appID,
|
||||
'state' => \json_encode($this->state),
|
||||
'redirect_uri' => $this->callback
|
||||
]);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getTokens(string $code): array
|
||||
{
|
||||
if (empty($this->tokens)) {
|
||||
$this->tokens = \json_decode($this->request(
|
||||
'POST',
|
||||
$this->apiEndpoint . '/oauth/token',
|
||||
['Content-Type: application/x-www-form-urlencoded'],
|
||||
\http_build_query([
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $code,
|
||||
'redirect_uri' => $this->callback,
|
||||
'client_id' => $this->appID,
|
||||
'client_secret' => $this->appSecret
|
||||
])
|
||||
), true);
|
||||
}
|
||||
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $refreshToken
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function refreshTokens(string $refreshToken): array
|
||||
{
|
||||
$this->tokens = \json_decode($this->request(
|
||||
'POST',
|
||||
$this->apiEndpoint . '/oauth/token',
|
||||
['Content-Type: application/x-www-form-urlencoded'],
|
||||
\http_build_query([
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshToken,
|
||||
'client_id' => $this->appID,
|
||||
'client_secret' => $this->appSecret,
|
||||
])
|
||||
), true);
|
||||
|
||||
if (empty($this->tokens['refresh_token'])) {
|
||||
$this->tokens['refresh_token'] = $refreshToken;
|
||||
}
|
||||
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserID(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return \strval($user['user_id']) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserEmail(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return $user['mail'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the OAuth email is verified
|
||||
*
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmailVerified(string $accessToken): bool
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
$mails = $user['mails'];
|
||||
$mainMailIndex = \array_search($user['mail'], \array_map(fn($m) => $m['mail'], $mails));
|
||||
$mainMain = $mails[$mainMailIndex];
|
||||
|
||||
if ($mainMain['verified'] ?? false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserName(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return $user['name'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getUser(string $accessToken): array
|
||||
{
|
||||
if (empty($this->user)) {
|
||||
$user = \json_decode($this->request(
|
||||
'GET',
|
||||
$this->apiEndpoint . '/user',
|
||||
['Authorization: Bearer ' . \urlencode($accessToken)]
|
||||
), true);
|
||||
|
||||
$profile = \json_decode($this->request(
|
||||
'GET',
|
||||
$this->apiEndpoint . '/user/profile',
|
||||
['Authorization: Bearer ' . \urlencode($accessToken)]
|
||||
), true);
|
||||
|
||||
$this->user = $user;
|
||||
$this->user['name'] = $profile['name'];
|
||||
}
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
}
|
|
@ -47,7 +47,8 @@ abstract class Migration
|
|||
'0.14.2' => 'V13',
|
||||
'0.15.0' => 'V14',
|
||||
'0.15.1' => 'V14',
|
||||
'0.15.2' => 'V14'
|
||||
'0.15.2' => 'V14',
|
||||
'0.15.3' => 'V14'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
namespace Appwrite\Migration\Version;
|
||||
|
||||
use Appwrite\Migration\Migration;
|
||||
use Utopia\App;
|
||||
use Exception;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class V14 extends Migration
|
||||
{
|
||||
|
@ -208,6 +209,14 @@ class V14 extends Migration
|
|||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
/**
|
||||
* Migrate Attributes
|
||||
*/
|
||||
$this->migrateAttributesAndCollections('attributes', $collection);
|
||||
/**
|
||||
* Migrate Indexes
|
||||
*/
|
||||
$this->migrateAttributesAndCollections('indexes', $collection);
|
||||
}, $document);
|
||||
}
|
||||
}, $documents);
|
||||
|
@ -220,6 +229,53 @@ class V14 extends Migration
|
|||
} while (!is_null($nextCollection));
|
||||
}
|
||||
|
||||
protected function migrateAttributesAndCollections(string $type, Document $collection): void
|
||||
{
|
||||
/**
|
||||
* Offset pagination instead of cursor, since documents are re-created!
|
||||
*/
|
||||
$offset = 0;
|
||||
$attributesCount = $this->projectDB->count($type, queries: [new Query('collectionId', Query::TYPE_EQUAL, [$collection->getId()])]);
|
||||
|
||||
do {
|
||||
$documents = $this->projectDB->find($type, limit: $this->limit, offset: $offset, queries: [new Query('collectionId', Query::TYPE_EQUAL, [$collection->getId()])]);
|
||||
$offset += $this->limit;
|
||||
|
||||
foreach ($documents as $document) {
|
||||
go(function (Document $document, string $internalId, string $type) {
|
||||
try {
|
||||
/**
|
||||
* Skip already migrated Documents.
|
||||
*/
|
||||
if (!is_null($document->getAttribute('databaseId'))) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Add Internal ID 'collectionInternalId' for Subqueries.
|
||||
*/
|
||||
$document->setAttribute('collectionInternalId', $internalId);
|
||||
/**
|
||||
* Add Internal ID 'databaseInternalId' for Subqueries.
|
||||
*/
|
||||
$document->setAttribute('databaseInternalId', '1');
|
||||
/**
|
||||
* Add Internal ID 'databaseId'.
|
||||
*/
|
||||
$document->setAttribute('databaseId', 'default');
|
||||
|
||||
/**
|
||||
* Re-create Attribute.
|
||||
*/
|
||||
$this->projectDB->deleteDocument($document->getCollection(), $document->getId());
|
||||
$this->projectDB->createDocument($document->getCollection(), $document->setAttribute('$id', "1_{$internalId}_{$document->getAttribute('key')}"));
|
||||
} catch (\Throwable $th) {
|
||||
Console::error("Failed to {$type} document: " . $th->getMessage());
|
||||
}
|
||||
}, $document, $collection->getInternalId(), $type);
|
||||
}
|
||||
} while ($offset < $attributesCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all Collections.
|
||||
*
|
||||
|
@ -580,40 +636,6 @@ class V14 extends Migration
|
|||
$document->setAttribute('teamInternalId', $internalId);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'attributes':
|
||||
case 'indexes':
|
||||
/**
|
||||
* Add Internal ID 'collectionId' for Subqueries.
|
||||
*/
|
||||
if (!empty($document->getAttribute('collectionId')) && is_null($document->getAttribute('collectionInternalId'))) {
|
||||
$internalId = $this->projectDB->getDocument('database_1', $document->getAttribute('collectionId'))->getInternalId();
|
||||
$document->setAttribute('collectionInternalId', $internalId);
|
||||
}
|
||||
/**
|
||||
* Add Internal ID 'databaseInternalId' for Subqueries.
|
||||
*/
|
||||
if (is_null($document->getAttribute('databaseInternalId'))) {
|
||||
$document->setAttribute('databaseInternalId', '1');
|
||||
}
|
||||
/**
|
||||
* Add Internal ID 'databaseInternalId' for Subqueries.
|
||||
*/
|
||||
if (is_null($document->getAttribute('databaseId'))) {
|
||||
$document->setAttribute('databaseId', 'default');
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* Re-create Collection Document
|
||||
*/
|
||||
$internalId = $this->projectDB->getDocument('database_1', $document->getAttribute('collectionId'))->getInternalId();
|
||||
$this->projectDB->deleteDocument($document->getCollection(), $document->getId());
|
||||
$this->projectDB->createDocument($document->getCollection(), $document->setAttribute('$id', "1_{$internalId}_{$document->getAttribute('key')}"));
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("Create Collection Document - {$th->getMessage()}");
|
||||
}
|
||||
$document = null;
|
||||
break;
|
||||
case 'platforms':
|
||||
/**
|
||||
|
|
|
@ -66,7 +66,7 @@ class Deployment extends Model
|
|||
])
|
||||
->addRule('status', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The deployment status.',
|
||||
'description' => 'The deployment status. Possible values are "processing", "building", "pending", "ready", and "failed".',
|
||||
'default' => '',
|
||||
'example' => 'enabled',
|
||||
])
|
||||
|
|
|
@ -107,6 +107,8 @@ trait TeamsBaseClient
|
|||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertNotEmpty($response['body']['userId']);
|
||||
$this->assertEquals($name, $response['body']['userName']);
|
||||
$this->assertEquals($email, $response['body']['userEmail']);
|
||||
$this->assertNotEmpty($response['body']['teamId']);
|
||||
$this->assertNotEmpty($response['body']['teamName']);
|
||||
$this->assertCount(2, $response['body']['roles']);
|
||||
|
|
|
@ -57,6 +57,8 @@ trait TeamsBaseServer
|
|||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertNotEmpty($response['body']['userId']);
|
||||
$this->assertEquals('Friend User', $response['body']['userName']);
|
||||
$this->assertEquals($email, $response['body']['userEmail']);
|
||||
$this->assertNotEmpty($response['body']['teamId']);
|
||||
$this->assertCount(2, $response['body']['roles']);
|
||||
$this->assertIsInt($response['body']['joined']);
|
||||
|
|
Loading…
Reference in a new issue