diff --git a/.gitignore b/.gitignore index 88407ef86f..c59f3450e6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ Icon *.log .cache .vscode +.ddev # Ignore dev files /.idea @@ -23,8 +24,6 @@ deploy.* # Ignore configuration files /config/* -!/config/migrations.php -!/config/migrations.upgrades.php !/config/api_sample.php # PHPUnit @@ -69,4 +68,4 @@ deploy.* public/cliserver.php # Cache generated while filesystem adapter -/cache/* \ No newline at end of file +/cache/* diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000000..d61b0e269d --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,36 @@ +image: gitpod/workspace-mysql + +tasks: + - init: > + composer install && + cp config/api_sample.php config/api.php && + echo "downloading demo database..." && + curl https://directus.github.io/demo-sql/demo.sql > demo.sql && + echo "please wait while demo database (demo.sql) is being imported..." && + mysql -u root < gitpod-helper.sql && + mysql -u root -p'root' directus < demo.sql && + rm demo.sql + + command: > + apachectl start && + echo "######################################################################################################" && + echo "# " && + echo "# Your Directus Backend is now running! You can access it with a Directus App by the following url:" && + echo "# " && + echo "# $GITPOD_WORKSPACE_URL/_/" | sed -e 's/https:\/\//https:\/\/8001-/g' && + echo "# " && + echo "# Username: admin@example.com Password: password" && + echo "# " && + echo "# " && + echo "# Quick access to Directus frontend app (choose 'other' in the api dropdown):" && + echo "# " && + echo "# Either use: https://directus.app/ " && + echo "# or" && + echo "# start a Directus App workspace: https://gitpod.io#https://github.com/directus/app" && + echo "# " && + echo "######################################################################################################" + + +ports: + - port: 8001 + diff --git a/README.md b/README.md index 1504f05856..ca7a23c5c9 100644 --- a/README.md +++ b/README.md @@ -131,11 +131,16 @@ Directus is a GPLv3-licensed open source project with development made possible ### Contributing -We love pull-requests! To work on Directus you'll need to install it locally from source by following the instructions below. Submit all pull-requests to the `master` branch of our `api` and `app` repositories. +We love pull-requests! To work on Directus you'll need to install it locally from source by following the instructions below. Submit all pull-requests to the `develop` branch of our `api` and `app` repositories. * [Setup API Development Environment](https://docs.directus.io/advanced/source.html#api-source) * [Setup App Development Environment](https://docs.directus.io/advanced/source.html#application-source) +If you want to dive right into the code and skip the manual setup of your development environment you can also spin up fully functional browser based development environments with a single click: + +* [Start API Gitpod Workspace](https://gitpod.io/#https://github.com/directus/api) +* [Start APP Gitpod Workspace](https://gitpod.io/#https://github.com/directus/app) + ### Sponsors [RANGER Studio](http://rangerstudio.com), Bas Jansen diff --git a/composer.json b/composer.json index 730fbe4d93..1d51b3f072 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ ], "require": { "php": "^7.1", - "slim/slim": "^3.0.0", + "slim/slim": "^3.12.2", "monolog/monolog": "^1.23.0", "zendframework/zend-db": "dev-directus", "league/flysystem": "^1.0", @@ -41,7 +41,8 @@ "ext-gd": "*", "webonyx/graphql-php": "^0.13.0", "char0n/ffmpeg-php": "^3.0.0", - "pragmarx/google2fa": "^5.0" + "pragmarx/google2fa": "^5.0", + "guzzlehttp/guzzle": "~6.0" }, "suggest": { "paragonie/random_compat": "Generates cryptographically more secure pseudo-random bytes", diff --git a/config/api_sample.php b/config/api_sample.php index 9e8877f707..760dfbc9a9 100644 --- a/config/api_sample.php +++ b/config/api_sample.php @@ -24,6 +24,11 @@ // When using unix socket to connect to the database the host attribute should be removed // 'socket' => '/var/lib/mysql/mysql.sock', 'socket' => '', + // Connect over TLS by using the appropriate PDO_MySQL constants: + // https://www.php.net/manual/en/ref.pdo-mysql.php#pdo-mysql.constants + //'driver_options' => [ + // PDO::MYSQL_ATTR_SSL_CAPATH => '/etc/ssl/certs', + //] ], 'cache' => [ diff --git a/gitpod-helper.sql b/gitpod-helper.sql new file mode 100644 index 0000000000..cf0a0213c1 --- /dev/null +++ b/gitpod-helper.sql @@ -0,0 +1,3 @@ +CREATE DATABASE IF NOT EXISTS directus; +USE directus; +SET PASSWORD FOR 'root'@'localhost' = PASSWORD('root'); \ No newline at end of file diff --git a/migrations/db/schemas/20180220023217_create_roles_table.php b/migrations/db/schemas/20180220023217_create_roles_table.php index c52c2802df..2f07fe0110 100644 --- a/migrations/db/schemas/20180220023217_create_roles_table.php +++ b/migrations/db/schemas/20180220023217_create_roles_table.php @@ -55,6 +55,10 @@ public function change() 'null' => true, 'default' => null ]); + $table->addColumn('enforce_2fa', 'boolean', [ + 'null' => true, + 'default' => false + ]); $table->addIndex('name', [ 'unique' => true, diff --git a/migrations/db/schemas/20180220023248_create_users_table.php b/migrations/db/schemas/20180220023248_create_users_table.php index a8e4e33953..3fa16eb4e1 100644 --- a/migrations/db/schemas/20180220023248_create_users_table.php +++ b/migrations/db/schemas/20180220023248_create_users_table.php @@ -66,7 +66,7 @@ public function change() $table->addColumn('locale', 'string', [ 'limit' => 8, 'null' => true, - 'default' => 'en-US' + 'default' => null ]); $table->addColumn('locale_options', 'text', [ 'null' => true, @@ -107,6 +107,13 @@ public function change() 'default' => null ]); + $table->addColumn('2fa_secret', 'string', [ + 'limit' => 100, + 'encoding' => 'utf8', + 'null' => true, + 'default' => null + ]); + $table->addIndex('email', [ 'unique' => true, 'name' => 'idx_users_email' diff --git a/migrations/db/schemas/20190912072543_create_user_sessions.php b/migrations/db/schemas/20190912072543_create_user_sessions.php new file mode 100644 index 0000000000..72aec69d50 --- /dev/null +++ b/migrations/db/schemas/20190912072543_create_user_sessions.php @@ -0,0 +1,76 @@ +table('directus_user_sessions', ['signed' => false]); + + $table->addColumn('user', 'integer', [ + 'signed' => false, + 'null' => true, + 'default' => null + ]); + + $table->addColumn('token_type', 'string', [ + 'null' => true, + 'default' => null + ]); + + $table->addColumn('token', 'string', [ + 'limit' => 520, + 'encoding' => 'utf8', + 'null' => true, + 'default' => null + ]); + + $table->addColumn('ip_address', 'string', [ + 'limit' => 255, + 'encoding' => 'utf8', + 'null' => true, + 'default' => null + ]); + + $table->addColumn('user_agent', 'text', [ + 'default' => null, + 'null' => true, + 'default' => null + ]); + + $table->addColumn('created_on', 'datetime', [ + 'null' => true, + 'default' => null + ]); + + $table->addColumn('token_expired_at', 'datetime', [ + 'null' => true, + 'default' => null + ]); + + $table->create(); + } +} diff --git a/migrations/db/schemas/20190917090849_create_web_hooks.php b/migrations/db/schemas/20190917090849_create_web_hooks.php new file mode 100644 index 0000000000..1be2ad47a3 --- /dev/null +++ b/migrations/db/schemas/20190917090849_create_web_hooks.php @@ -0,0 +1,68 @@ +table('directus_webhooks', ['signed' => false]); + + $table->addColumn('status', 'string', [ + 'limit' => 16, + 'default' => \Directus\Api\Routes\Webhook::STATUS_INACTIVE + ]); + + + $table->addColumn('http_action', 'string', [ + 'limit' => 255, + 'encoding' => 'utf8', + 'null' => true, + 'default' => null + ]); + + $table->addColumn('url', 'string', [ + 'limit' => 510, + 'encoding' => 'utf8', + 'null' => true, + 'default' => null + ]); + + $table->addColumn('collection', 'string', [ + 'limit' => 255, + 'null' => true, + 'default' => null + ]); + + $table->addColumn('directus_action', 'string', [ + 'limit' => 255, + 'encoding' => 'utf8', + 'null' => true, + 'default' => null + ]); + + $table->create(); + } +} diff --git a/migrations/db/seeds/CollectionPresetsSeeder.php b/migrations/db/seeds/CollectionPresetsSeeder.php index cc023ed033..65fae06913 100644 --- a/migrations/db/seeds/CollectionPresetsSeeder.php +++ b/migrations/db/seeds/CollectionPresetsSeeder.php @@ -17,7 +17,6 @@ public function run() $data = [ [ 'collection' => 'directus_activity', - 'view_type' => 'tabular', 'view_type' => 'timeline', 'view_query' => json_encode([ 'timeline' => [ @@ -57,6 +56,26 @@ public function run() 'icon' => 'person' ] ]) + ], + [ + 'collection' => 'directus_webhooks', + 'view_type' => 'tabular', + 'view_query' => json_encode([ + 'tabular' => [ + 'fields' => 'status,http_action,url,collection,directus_action' + ] + ]), + 'view_options' => json_encode([ + 'tabular' => [ + 'widths' => [ + 'status' => 32, + 'http_action' => 72, + 'url' => 200, + 'collection' => 200, + 'directus_action' => 200 + ] + ] + ]) ] ]; diff --git a/migrations/db/seeds/FieldsSeeder.php b/migrations/db/seeds/FieldsSeeder.php index 3e608d8096..a090faa582 100644 --- a/migrations/db/seeds/FieldsSeeder.php +++ b/migrations/db/seeds/FieldsSeeder.php @@ -33,15 +33,22 @@ public function run() 'field' => 'action', 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'activity-icon', + 'options' => json_encode([ + 'iconRight' => 'change_history' + ]), 'locked' => 1, 'readonly' => 1, - 'sort' => 1 + 'sort' => 1, + 'width' => 'full' ], [ 'collection' => 'directus_activity', 'field' => 'collection', 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'collections', + 'options' => json_encode([ + 'iconRight' => 'list_alt' + ]), 'locked' => 1, 'readonly' => 1, 'sort' => 2, @@ -52,6 +59,9 @@ public function run() 'field' => 'item', 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'text-input', + 'options' => json_encode([ + 'iconRight' => 'link' + ]), 'locked' => 1, 'readonly' => 1, 'sort' => 3, @@ -62,6 +72,9 @@ public function run() 'field' => 'action_by', 'type' => \Directus\Database\Schema\DataTypes::TYPE_INTEGER, 'interface' => 'user', + 'options' => json_encode([ + 'iconRight' => 'account_circle' + ]), 'locked' => 1, 'readonly' => 1, 'sort' => 4, @@ -73,7 +86,8 @@ public function run() 'type' => \Directus\Database\Schema\DataTypes::TYPE_DATETIME, 'interface' => 'datetime', 'options' => json_encode([ - 'showRelative' => true + 'showRelative' => true, + 'iconRight' => 'calendar_today' ]), 'locked' => 1, 'readonly' => 1, @@ -86,7 +100,8 @@ public function run() 'type' => \Directus\Database\Schema\DataTypes::TYPE_DATETIME, 'interface' => 'datetime', 'options' => json_encode([ - 'showRelative' => true + 'showRelative' => true, + 'iconRight' => 'edit' ]), 'locked' => 1, 'readonly' => 1, @@ -99,7 +114,8 @@ public function run() 'type' => \Directus\Database\Schema\DataTypes::TYPE_DATETIME, 'interface' => 'datetime', 'options' => json_encode([ - 'showRelative' => true + 'showRelative' => true, + 'iconRight' => 'delete_outline' ]), 'locked' => 1, 'readonly' => 1, @@ -111,6 +127,9 @@ public function run() 'field' => 'ip', 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'text-input', + 'options' => json_encode([ + 'iconRight' => 'my_location' + ]), 'locked' => 1, 'readonly' => 1, 'sort' => 8, @@ -121,6 +140,9 @@ public function run() 'field' => 'user_agent', 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'text-input', + 'options' => json_encode([ + 'iconRight' => 'devices_other' + ]), 'locked' => 1, 'readonly' => 1, 'sort' => 9, @@ -133,7 +155,8 @@ public function run() 'interface' => 'textarea', 'locked' => 1, 'readonly' => 1, - 'sort' => 10 + 'sort' => 10, + 'width' => 'full' ], @@ -427,8 +450,7 @@ public function run() 'type' => \Directus\Database\Schema\DataTypes::TYPE_ALIAS, 'interface' => 'file', 'locked' => 1, - 'hidden_detail' => 1, - 'sort' => 0 + 'hidden_detail' => 1 ], [ 'collection' => 'directus_files', @@ -437,8 +459,7 @@ public function run() 'interface' => 'primary-key', 'locked' => 1, 'required' => 1, - 'hidden_detail' => 1, - 'sort' => 1 + 'hidden_detail' => 1 ], [ 'collection' => 'directus_files', @@ -446,7 +467,8 @@ public function run() 'type' => \Directus\Database\Schema\DataTypes::TYPE_ALIAS, 'interface' => 'file-preview', 'locked' => 1, - 'sort' => 2 + 'sort' => 1, + 'width' => 'full' ], [ 'collection' => 'directus_files', @@ -481,6 +503,9 @@ public function run() 'field' => 'tags', 'type' => \Directus\Database\Schema\DataTypes::TYPE_ARRAY, 'interface' => 'tags', + 'options' => json_encode([ + 'placeholder' => 'Enter a keyword then hit enter...' + ]), 'sort' => 5, 'width' => 'half' ], @@ -502,9 +527,35 @@ public function run() 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'wysiwyg', 'options' => json_encode([ - 'placeholder' => 'Enter a caption or description...' + 'toolbar' => ['bold','italic','underline','link','code'] ]), - 'sort' => 7 + 'sort' => 7, + 'width' => 'full' + ], + [ + 'collection' => 'directus_files', + 'field' => 'uploaded_on', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_DATETIME, + 'interface' => 'datetime', + 'options' => json_encode([ + 'iconRight' => 'today' + ]), + 'locked' => 1, + 'readonly' => 1, + 'sort' => 8, + 'width' => 'half', + 'required' => 1 + ], + [ + 'collection' => 'directus_files', + 'field' => 'uploaded_by', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_USER_CREATED, + 'interface' => 'user-created', + 'locked' => 1, + 'readonly' => 1, + 'sort' => 9, + 'width' => 'half', + 'required' => 1 ], [ 'collection' => 'directus_files', @@ -558,38 +609,14 @@ public function run() 'sort' => 13, 'width' => 'half' ], - [ - 'collection' => 'directus_files', - 'field' => 'uploaded_on', - 'type' => \Directus\Database\Schema\DataTypes::TYPE_DATETIME, - 'interface' => 'datetime', - 'options' => json_encode([ - 'iconRight' => 'today' - ]), - 'locked' => 1, - 'readonly' => 1, - 'sort' => 8, - 'width' => 'half', - 'required' => 1 - ], - [ - 'collection' => 'directus_files', - 'field' => 'uploaded_by', - 'type' => \Directus\Database\Schema\DataTypes::TYPE_INTEGER, - 'interface' => 'user', - 'locked' => 1, - 'readonly' => 1, - 'sort' => 9, - 'width' => 'half', - 'required' => 1 - ], [ 'collection' => 'directus_files', 'field' => 'metadata', 'type' => \Directus\Database\Schema\DataTypes::TYPE_JSON, 'interface' => 'json', 'locked' => 1, - 'sort' => 14 + 'sort' => 14, + 'width' => 'full' ], [ 'collection' => 'directus_files', @@ -994,52 +1021,119 @@ public function run() 'field' => 'project_name', 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'text-input', + 'options' => json_encode([ + 'iconRight' => 'title' + ]), 'locked' => 1, 'required' => 1, - 'width' => 'half-space', + 'width' => 'half', + 'note' => 'Logo in the top-left of the App (40x40)', 'sort' => 1 ], [ 'collection' => 'directus_settings', - 'field' => 'color', + 'field' => 'project_url', 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, - 'interface' => 'color-palette', + 'interface' => 'text-input', + 'options' => json_encode([ + 'iconRight' => 'link' + ]), 'locked' => 1, 'width' => 'half', - 'note' => 'The color that best fits your brand.', + 'note' => 'External link for the App\'s top-left logo', 'sort' => 2 ], [ 'collection' => 'directus_settings', - 'field' => 'logo', + 'field' => 'project_logo', 'type' => \Directus\Database\Schema\DataTypes::TYPE_FILE, 'interface' => 'file', 'locked' => 1, 'width' => 'half', - 'note' => 'Your brand\'s logo.', + 'note' => 'A 40x40 brand logo, ideally a white SVG/PNG', 'sort' => 3 ], [ 'collection' => 'directus_settings', - 'field' => 'app_url', + 'field' => 'project_color', 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, - 'interface' => 'text-input', + 'interface' => 'color-palette', 'locked' => 1, - 'required' => 1, - 'width' => 'half-space', - 'note' => 'The URL where your app is hosted. The API will use this to direct your users to the correct login page.', + 'width' => 'half', + 'note' => 'Color for login background and App\'s logo', 'sort' => 4 ], + [ + 'collection' => 'directus_settings', + 'field' => 'project_foreground', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_FILE, + 'interface' => 'file', + 'locked' => 1, + 'width' => 'half', + 'note' => 'Centered image (eg: logo) for the login page', + 'sort' => 5 + ], + [ + 'collection' => 'directus_settings', + 'field' => 'project_background', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_FILE, + 'interface' => 'file', + 'locked' => 1, + 'width' => 'half', + 'note' => 'Full-screen background for the login page', + 'sort' => 6 + ], + [ + 'collection' => 'directus_settings', + 'field' => 'default_locale', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'language', + 'locked' => 1, + 'width' => 'half', + 'note' => 'Default locale for Directus Users', + 'sort' => 7, + 'options' => json_encode([ + 'limit' => true + ]) + ], + [ + 'collection' => 'directus_settings', + 'field' => 'telemetry', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_BOOLEAN, + 'interface' => 'toggle', + 'locked' => 1, + 'width' => 'half', + 'note' => 'Learn More', + 'sort' => 8 + ], + [ + 'collection' => 'directus_settings', + 'field' => 'data_divider', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_ALIAS, + 'interface' => 'divider', + 'options' => json_encode([ + 'style' => 'large', + 'title' => 'Data', + 'hr' => true + ]), + 'locked' => 1, + 'width' => 'full', + 'hidden_browse' => 1, + 'sort' => 10 + ], [ 'collection' => 'directus_settings', 'field' => 'default_limit', 'type' => \Directus\Database\Schema\DataTypes::TYPE_INTEGER, 'interface' => 'numeric', + 'options' => json_encode([ + 'iconRight' => 'keyboard_tab' + ]), 'locked' => 1, 'required' => 1, 'width' => 'half', - 'note' => 'Default max amount of items that\'s returned at a time in the API.', - 'sort' => 5 + 'note' => 'Default item count in API and App responses', + 'sort' => 11 ], [ 'collection' => 'directus_settings', @@ -1047,40 +1141,120 @@ public function run() 'type' => \Directus\Database\Schema\DataTypes::TYPE_BOOLEAN, 'interface' => 'toggle', 'locked' => 1, - 'note' => 'Will sort values with null at the end of the result', + 'note' => 'NULL values are sorted last', 'width' => 'half', - 'note' => 'Put items with `null` for the value last when sorting.', - 'sort' => 6 + 'sort' => 12 + ], + [ + 'collection' => 'directus_settings', + 'field' => 'security_divider', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_ALIAS, + 'interface' => 'divider', + 'options' => json_encode([ + 'style' => 'large', + 'title' => 'Security', + 'hr' => true + ]), + 'locked' => 1, + 'hidden_browse' => 1, + 'width' => 'full', + 'sort' => 20 ], [ 'collection' => 'directus_settings', 'field' => 'auto_sign_out', 'type' => \Directus\Database\Schema\DataTypes::TYPE_INTEGER, 'interface' => 'numeric', + 'options' => json_encode([ + 'iconRight' => 'timer' + ]), 'locked' => 1, 'required' => 1, 'width' => 'half', - 'note' => 'How many minutes before an idle user is signed out.', - 'sort' => 7 + 'note' => 'Minutes before idle users are signed out', + 'sort' => 22 ], [ 'collection' => 'directus_settings', - 'field' => 'youtube_api', + 'field' => 'login_attempts_allowed', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_INTEGER, + 'interface' => 'numeric', + 'options' => json_encode([ + 'iconRight' => 'lock' + ]), + 'locked' => 1, + 'width' => 'half', + 'note' => 'Failed login attempts before suspending users', + 'sort' => 23 + ], + [ + 'collection' => 'directus_settings', + 'field' => 'files_divider', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_ALIAS, + 'interface' => 'divider', + 'options' => json_encode([ + 'style' => 'large', + 'title' => 'Files & Thumbnails', + 'hr' => true + ]), + 'locked' => 1, + 'hidden_browse' => 1, + 'width' => 'full', + 'sort' => 30 + ], + [ + 'collection' => 'directus_settings', + 'field' => 'file_naming', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'dropdown', + 'locked' => 1, + 'width' => 'half', + 'note' => 'File-system naming convention for uploads', + 'sort' => 31, + 'options' => json_encode([ + 'choices' => [ + 'uuid' => 'File Hash (Obfuscated)', + 'file_name' => 'File Name (Readable)' + ] + ]) + ], + [ + 'collection' => 'directus_settings', + 'field' => 'file_max_size', 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'text-input', + 'options' => json_encode([ + 'placeholder' => 'eg: 4MB', + 'iconRight' => 'storage' + ]), 'locked' => 1, 'width' => 'half', - 'note' => 'When provided, this allows more information to be collected for YouTube embeds.', - 'sort' => 8 + 'sort' => 32 + ], + [ + 'collection' => 'directus_settings', + 'field' => 'file_mimetype_whitelist', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_ARRAY, + 'interface' => 'tags', + 'options' => json_encode([ + 'placeholder' => 'Enter a file mimetype then hit enter (eg: image/jpeg)' + ]), + 'locked' => 1, + 'width' => 'full', + 'sort' => 33 ], [ 'collection' => 'directus_settings', 'field' => 'thumbnail_dimensions', 'type' => \Directus\Database\Schema\DataTypes::TYPE_ARRAY, 'interface' => 'tags', + 'options' => json_encode([ + 'placeholder' => 'Allowed dimensions for thumbnails (eg: 200x200)' + ]), 'locked' => 1, + 'width' => 'full', 'note' => 'Allowed dimensions for thumbnails.', - 'sort' => 9 + 'sort' => 34 ], [ 'collection' => 'directus_settings', @@ -1089,8 +1263,8 @@ public function run() 'interface' => 'json', 'locked' => 1, 'width' => 'half', - 'note' => 'Allowed quality for thumbnails.', - 'sort' => 10 + 'note' => 'Allowed qualities for thumbnails', + 'sort' => 35 ], [ 'collection' => 'directus_settings', @@ -1099,47 +1273,49 @@ public function run() 'interface' => 'json', 'locked' => 1, 'width' => 'half', - 'note' => 'Defines how the thumbnail will be generated based on the requested dimensions.', - 'sort' => 11 + 'note' => 'Defines how the thumbnail will be generated based on the requested dimensions', + 'sort' => 36 + ], + [ + 'collection' => 'directus_settings', + 'field' => 'thumbnail_not_found_location', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'text-input', + 'options' => json_encode([ + 'iconRight' => 'broken_image' + ]), + 'locked' => 1, + 'width' => 'full', + 'note' => 'A fallback image used when thumbnail generation fails', + 'sort' => 37 ], [ 'collection' => 'directus_settings', 'field' => 'thumbnail_cache_ttl', 'type' => \Directus\Database\Schema\DataTypes::TYPE_INTEGER, 'interface' => 'numeric', + 'options' => json_encode([ + 'iconRight' => 'cached' + ]), 'locked' => 1, 'width' => 'half', 'required' => 1, - 'note' => '`max-age` HTTP header of the thumbnail.', - 'sort' => 12 + 'note' => 'Seconds before browsers re-fetch thumbnails', + 'sort' => 38 ], [ 'collection' => 'directus_settings', - 'field' => 'thumbnail_not_found_location', + 'field' => 'youtube_api', 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'text-input', + 'options' => json_encode([ + 'iconRight' => 'videocam' + ]), 'locked' => 1, 'width' => 'half', - 'note' => 'This image will be used when trying to generate a thumbnail with invalid options or an error happens on the server when creating the image.', - 'sort' => 13 + 'note' => 'Allows fetching more YouTube Embed info', + 'sort' => 39 ], - [ - 'collection' => 'directus_settings', - 'field' => 'file_naming', - 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, - 'interface' => 'dropdown', - 'locked' => 1, - 'width' => 'full', - 'note' => 'The file-system naming convention for uploads.', - 'sort' => 14, - 'options' => json_encode([ - 'choices' => [ - 'uuid' => 'File Hash (Obfuscated)', - 'file_name' => 'File Name (Readable)' - ] - ]) - ], - // Users // ----------------------------------------------------------------- @@ -1212,7 +1388,7 @@ public function run() 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'text-input', 'options' => json_encode([ - 'placeholder' => 'Enter your give name...' + 'iconRight' => 'account_circle' ]), 'locked' => 1, 'required' => 1, @@ -1225,7 +1401,7 @@ public function run() 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'text-input', 'options' => json_encode([ - 'placeholder' => 'Enter your surname...' + 'iconRight' => 'account_circle' ]), 'locked' => 1, 'required' => 1, @@ -1238,7 +1414,7 @@ public function run() 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'text-input', 'options' => json_encode([ - 'placeholder' => 'Enter your email address...' + 'iconRight' => 'alternate_email' ]), 'locked' => 1, 'validation' => '$email', @@ -1281,7 +1457,7 @@ public function run() 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'text-input', 'options' => json_encode([ - 'placeholder' => 'Enter your company or organization name...' + 'iconRight' => 'location_city' ]), 'sort' => 9, 'width' => 'half' @@ -1292,7 +1468,7 @@ public function run() 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, 'interface' => 'text-input', 'options' => json_encode([ - 'placeholder' => 'Enter your title or role...' + 'iconRight' => 'text_fields' ]), 'sort' => 10, 'width' => 'half' @@ -1468,7 +1644,24 @@ public function run() 'locked' => 1, 'sort' => 12, 'width' => 'half', - 'required' => 1 + 'required' => 0 + ], + [ + 'collection' => 'directus_users', + 'field' => 'avatar', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_FILE, + 'interface' => 'file', + 'locked' => 1, + 'sort' => 13 + ], + [ + 'collection' => 'directus_users', + 'field' => '2fa_secret', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => '2fa-secret', + 'locked' => 1, + 'readonly' => 1, + 'sort' => 14 ], [ 'collection' => 'directus_users', @@ -1478,7 +1671,7 @@ public function run() 'locked' => 1, 'hidden_browse' => 1, 'hidden_detail' => 1, - 'sort' => 13 + 'sort' => 15 ], [ 'collection' => 'directus_users', @@ -1488,17 +1681,7 @@ public function run() 'locked' => 1, 'hidden_detail' => 1, 'hidden_browse' => 1, - 'sort' => 14 - ], - [ - 'collection' => 'directus_users', - 'field' => 'last_login', - 'type' => \Directus\Database\Schema\DataTypes::TYPE_DATETIME, - 'interface' => 'datetime', - 'locked' => 1, - 'readonly' => 1, - 'sort' => 15, - 'width' => 'half' + 'sort' => 16 ], [ 'collection' => 'directus_users', @@ -1508,8 +1691,7 @@ public function run() 'locked' => 1, 'readonly' => 1, 'hidden_detail' => 1, - 'sort' => 16, - 'width' => 'half' + 'sort' => 17 ], [ 'collection' => 'directus_users', @@ -1520,44 +1702,8 @@ public function run() 'readonly' => 1, 'hidden_detail' => 1, 'hidden_browse' => 1, - 'sort' => 17, - 'width' => 'half' - ], - [ - 'collection' => 'directus_users', - 'field' => 'avatar', - 'type' => \Directus\Database\Schema\DataTypes::TYPE_FILE, - 'interface' => 'file', - 'locked' => 1, 'sort' => 18 ], - [ - 'collection' => 'directus_users', - 'field' => 'invite_token', - 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, - 'interface' => 'text-input', - 'locked' => 1, - 'hidden_detail' => 1, - 'hidden_browse' => 1 - ], - [ - 'collection' => 'directus_users', - 'field' => 'invite_accepted', - 'type' => \Directus\Database\Schema\DataTypes::TYPE_BOOLEAN, - 'interface' => 'toggle', - 'locked' => 1, - 'hidden_detail' => 1, - 'hidden_browse' => 1 - ], - [ - 'collection' => 'directus_users', - 'field' => 'last_ip', - 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, - 'interface' => 'text-input', - 'locked' => 1, - 'readonly' => 1, - 'hidden_detail' => 1 - ], [ 'collection' => 'directus_users', 'field' => 'external_id', @@ -1567,14 +1713,6 @@ public function run() 'readonly' => 1, 'hidden_detail' => 1 ], - [ - 'collection' => 'directus_users', - 'field' => '2fa_secret', - 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, - 'interface' => '2fa-secret', - 'locked' => 1, - 'readonly' => 1 - ], // User Roles Junction // ----------------------------------------------------------------- @@ -1606,7 +1744,184 @@ public function run() 'field' => 'enforce_2fa', 'type' => \Directus\Database\Schema\DataTypes::TYPE_BOOLEAN, 'interface' => 'toggle' - ] + ], + + // User Session + // ----------------------------------------------------------------- + [ + 'collection' => 'directus_user_sessions', + 'field' => 'id', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_INTEGER, + 'interface' => 'primary-key', + 'locked' => 1, + 'required' => 1, + 'hidden_detail' => 1 + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'user', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_USER, + 'required' => 1, + 'interface' => 'user' + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'token_type', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'text-input' + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'token', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'text-input' + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'ip_address', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'text-input' + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'user_agent', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'text-input' + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'created_on', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_DATETIME, + 'interface' => 'datetime' + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'token_expired_at', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_DATETIME, + 'interface' => 'datetime' + ], + + // Webhooks + // ----------------------------------------------------------------- + [ + 'collection' => 'directus_webhooks', + 'field' => 'id', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_INTEGER, + 'interface' => 'primary-key', + 'locked' => 1, + 'required' => 1, + 'hidden_detail' => 1 + ], + [ + 'collection' => 'directus_webhooks', + 'field' => 'status', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STATUS, + 'interface' => 'status', + 'options' => json_encode([ + 'status_mapping' => [ + 'active' => [ + 'name' => 'Active', + 'value' => 'active', + 'text_color' => 'white', + 'background_color' => 'green', + 'browse_subdued' => false, + 'browse_badge' => true, + 'soft_delete' => false, + 'published' => true, + ], + 'inactive' => [ + 'name' => 'Inactive', + 'value' => 'inactive', + 'text_color' => 'white', + 'background_color' => 'blue-grey', + 'browse_subdued' => true, + 'browse_badge' => true, + 'soft_delete' => false, + 'published' => false, + ] + ] + ]), + 'locked' => 1, + 'width' => 'full', + 'sort' => 1 + ], + [ + 'collection' => 'directus_webhooks', + 'field' => 'http_action', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'dropdown', + 'required' => 1, + 'options' => json_encode([ + 'choices' => [ + 'get' => 'GET', + 'post' => 'POST' + ] + ]), + 'locked' => 1, + 'width' => 'half-space', + 'sort' => 2 + ], + [ + 'collection' => 'directus_webhooks', + 'field' => 'url', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'text-input', + 'options' => json_encode([ + 'placeholder' => 'https://example.com', + 'iconRight' => 'link' + ]), + 'required' => 1, + 'locked' => 1, + 'width' => 'full', + 'note' => '', + 'sort' => 3 + ], + [ + 'collection' => 'directus_webhooks', + 'field' => 'collection', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'collections', + 'required' => 1, + 'locked' => 1, + 'width' => 'half', + 'note' => '', + 'sort' => 4 + ], + [ + 'collection' => 'directus_webhooks', + 'field' => 'directus_action', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'dropdown', + 'required' => 1, + 'options' => json_encode([ + 'choices' => [ + 'item.create:after' => 'Create', + 'item.update:after' => 'Update', + 'item.delete:after' => 'Delete', + ] + ]), + 'locked' => 1, + 'width' => 'half', + 'note' => '', + 'sort' => 5 + ], + [ + 'collection' => 'directus_webhooks', + 'field' => 'info', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_ALIAS, + 'interface' => 'divider', + 'options' => json_encode([ + 'style' => 'medium', + 'title' => 'How Webhooks Work', + 'hr' => true, + 'margin' => false, + 'description' => 'When the selected action occurs for the selected collection, Directus will send an HTTP request to the above URL.' + ]), + 'locked' => 1, + 'width' => 'full', + 'hidden_browse' => 1, + 'sort' => 6 + ], ]; $files = $this->table('directus_fields'); diff --git a/migrations/db/seeds/RelationsSeeder.php b/migrations/db/seeds/RelationsSeeder.php index 02f994a00d..8ec9cd06f6 100644 --- a/migrations/db/seeds/RelationsSeeder.php +++ b/migrations/db/seeds/RelationsSeeder.php @@ -20,16 +20,6 @@ public function run() 'field_many' => 'action_by', 'collection_one' => 'directus_users' ], - [ - 'collection_many' => 'directus_activity_seen', - 'field_many' => 'user', - 'collection_one' => 'directus_users' - ], - [ - 'collection_many' => 'directus_activity_seen', - 'field_many' => 'activity', - 'collection_one' => 'directus_activity' - ], [ 'collection_many' => 'directus_collections_presets', 'field_many' => 'user', @@ -40,6 +30,12 @@ public function run() 'field_many' => 'group', 'collection_one' => 'directus_groups' ], + [ + 'collection_many' => 'directus_fields', + 'field_many' => 'collection', + 'collection_one' => 'directus_collections', + 'field_one' => 'fields' + ], [ 'collection_many' => 'directus_files', 'field_many' => 'uploaded_by', @@ -83,12 +79,6 @@ public function run() 'collection_many' => 'directus_users', 'field_many' => 'avatar', 'collection_one' => 'directus_files' - ], - [ - 'collection_many' => 'directus_fields', - 'field_many' => 'collection', - 'collection_one' => 'directus_collections', - 'field_one' => 'fields' ] ]; diff --git a/migrations/db/seeds/RolesSeeder.php b/migrations/db/seeds/RolesSeeder.php index ea2b7e1d90..b245c76522 100644 --- a/migrations/db/seeds/RolesSeeder.php +++ b/migrations/db/seeds/RolesSeeder.php @@ -23,7 +23,7 @@ public function run() [ 'id' => 2, 'name' => 'Public', - 'description' => 'This sets the data that is publicly available through the API without a token' + 'description' => 'Controls what API data is publicly available without authenticating' ] ]; diff --git a/migrations/db/seeds/SettingsSeeder.php b/migrations/db/seeds/SettingsSeeder.php index e8cbd8e112..86b1920296 100644 --- a/migrations/db/seeds/SettingsSeeder.php +++ b/migrations/db/seeds/SettingsSeeder.php @@ -16,12 +16,32 @@ public function run() { $data = [ [ - 'key' => 'logo', + 'key' => 'project_url', 'value' => '' ], [ - 'key' => 'color', - 'value' => 'darkest-gray', + 'key' => 'project_logo', + 'value' => '' + ], + [ + 'key' => 'project_color', + 'value' => 'blue-grey-900', + ], + [ + 'key' => 'project_foreground', + 'value' => '', + ], + [ + 'key' => 'project_background', + 'value' => '', + ], + [ + 'key' => 'default_locale', + 'value' => 'en-US', + ], + [ + 'key' => 'telemetry', + 'value' => '1', ], [ 'key' => 'default_limit', @@ -31,18 +51,34 @@ public function run() 'key' => 'sort_null_last', 'value' => '1' ], + [ + 'key' => 'password_policy', + 'value' => '' + ], [ 'key' => 'auto_sign_out', - 'value' => '60' + 'value' => '10080' ], [ - 'key' => 'youtube_api_key', - 'value' => '' + 'key' => 'login_attempts_allowed', + 'value' => '10' ], [ 'key' => 'trusted_proxies', 'value' => '' ], + [ + 'key' => 'file_naming', + 'value' => 'uuid' + ], + [ + 'key' => 'file_max_size', + 'value' => '100MB' + ], + [ + 'key' => 'file_mimetype_whitelist', + 'value' => '' + ], [ 'key' => 'thumbnail_dimensions', 'value' => '200x200' @@ -55,17 +91,17 @@ public function run() 'key' => 'thumbnail_actions', 'value' => '{"contain":{"options":{"resizeCanvas":false,"position":"center","resizeRelative":false,"canvasBackground":"ccc"}},"crop":{"options":{"position":"center"}}}' ], + [ + 'key' => 'thumbnail_not_found_location', + 'value' => '' + ], [ 'key' => 'thumbnail_cache_ttl', 'value' => '86400' ], [ - 'key' => 'thumbnail_not_found_location', + 'key' => 'youtube_api_key', 'value' => '' - ], - [ - 'key' => 'file_naming', - 'value' => 'uuid' ] ]; diff --git a/config/migrations.php b/migrations/migrations.php similarity index 100% rename from config/migrations.php rename to migrations/migrations.php diff --git a/config/migrations.upgrades.php b/migrations/migrations.upgrades.php similarity index 100% rename from config/migrations.upgrades.php rename to migrations/migrations.upgrades.php diff --git a/migrations/upgrades/schemas/20190422131600_use_json.php b/migrations/upgrades/schemas/20190422131600_use_json.php index ba2ee9d67c..dd26ee3f69 100644 --- a/migrations/upgrades/schemas/20190422131600_use_json.php +++ b/migrations/upgrades/schemas/20190422131600_use_json.php @@ -15,13 +15,13 @@ public function up() ['collection' => 'directus_activity', 'interface' => 'code'] )); - $this->execute(\Directus\phinx_update( - $this->getAdapter(), - 'directus_fields', - [ - 'interface' => 'json' - ], - ['collection' => 'directus_activity_seen', 'interface' => 'code'] + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'interface' => 'json' + ], + ['collection' => 'directus_activity_seen', 'interface' => 'code'] )); $this->execute(\Directus\phinx_update( diff --git a/migrations/upgrades/schemas/20190912072543_create_user_sessions.php b/migrations/upgrades/schemas/20190912072543_create_user_sessions.php new file mode 100644 index 0000000000..affa1768df --- /dev/null +++ b/migrations/upgrades/schemas/20190912072543_create_user_sessions.php @@ -0,0 +1,137 @@ +table('directus_user_sessions', ['signed' => false]); + + $table->addColumn('user', 'integer', [ + 'signed' => false, + 'null' => true, + 'default' => null + ]); + + $table->addColumn('token', 'string', [ + 'limit' => 520, + 'encoding' => 'utf8', + 'null' => true, + 'default' => null + ]); + + $table->addColumn('ip_address', 'string', [ + 'limit' => 255, + 'encoding' => 'utf8', + 'null' => true, + 'default' => null + ]); + + $table->addColumn('user_agent', 'text', [ + 'default' => null, + 'null' => true, + 'default' => null + ]); + + $table->addColumn('created_on', 'datetime', [ + 'null' => true, + 'default' => null + ]); + + $table->create(); + + // Insert Into Directus Fields + $data = [ + // User Session + // ----------------------------------------------------------------- + [ + 'collection' => 'directus_user_sessions', + 'field' => 'id', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_INTEGER, + 'interface' => 'primary-key', + 'locked' => 1, + 'required' => 1, + 'hidden_detail' => 1 + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'user', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_USER, + 'required' => 1, + 'interface' => 'user' + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'token_type', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'text-input' + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'token', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'text-input' + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'ip_address', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'text-input' + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'user_agent', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'text-input' + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'created_on', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_DATETIME, + 'interface' => 'datetime' + ], + [ + 'collection' => 'directus_user_sessions', + 'field' => 'token_expired_at', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_DATETIME, + 'interface' => 'datetime' + ], + ]; + + foreach($data as $value){ + if(!$this->checkFieldExist($value['collection'], $value['field'])){ + $insertSqlFormat = "INSERT INTO `directus_fields` (`collection`, `field`, `type`, `interface`, `hidden_detail`, `required`, `locked`, `options`) VALUES ('%s', '%s', '%s', '%s', '%s', '%s','%s' , '%s');"; + $insertSql = sprintf($insertSqlFormat,$value['collection'], $value['field'], $value['type'], $value['interface'], isset($value['hidden_detail']) ? $value['hidden_detail'] : 0, isset($value['required']) ? $value['required'] : 0, isset($value['locked']) ? $value['locked'] : 0, isset($value['options']) ? $value['options'] : null); + $this->execute($insertSql); + } + } + } + public function checkFieldExist($collection,$field){ + $checkSql = sprintf('SELECT 1 FROM `directus_fields` WHERE `collection` = "%s" AND `field` = "%s";', $collection, $field); + return $this->query($checkSql)->fetch(); + } + +} diff --git a/migrations/db/schemas/20180220023144_create_activity_seen_table.php b/migrations/upgrades/schemas/20190914095509_update_directus_user_sessions.php similarity index 52% rename from migrations/db/schemas/20180220023144_create_activity_seen_table.php rename to migrations/upgrades/schemas/20190914095509_update_directus_user_sessions.php index 4085da03a8..35c826317a 100644 --- a/migrations/db/schemas/20180220023144_create_activity_seen_table.php +++ b/migrations/upgrades/schemas/20190914095509_update_directus_user_sessions.php @@ -1,8 +1,9 @@ table('directus_activity_seen', ['signed' => false]); - - $table->addColumn('activity', 'integer', [ - 'null' => false, - 'signed' => false - ]); - - $table->addColumn('user', 'integer', [ - 'signed' => false, - 'null' => false, - 'default' => 0 - ]); - - $table->addColumn('seen_on', 'datetime', [ - 'null' => true, - 'default' => null - ]); - - $table->addColumn('archived', 'boolean', [ - 'signed' => false, - 'default' => false - ]); - - $table->create(); + $table = $this->table('directus_user_sessions'); + if (!$table->hasColumn('token_type')) { + $table->addColumn('token_type', 'string', [ + 'null' => true, + 'default' => null + ]); + } + + if (!$table->hasColumn('token_expired_at')) { + $table->addColumn('token_expired_at', 'datetime', [ + 'null' => true, + 'default' => null + ]); + } + + $table->save(); } } diff --git a/migrations/upgrades/schemas/20190917090849_create_web_hooks.php b/migrations/upgrades/schemas/20190917090849_create_web_hooks.php new file mode 100644 index 0000000000..374a3c28fb --- /dev/null +++ b/migrations/upgrades/schemas/20190917090849_create_web_hooks.php @@ -0,0 +1,183 @@ + +table('directus_webhooks', ['signed' => false]); + + $table->addColumn('status', 'string', [ + 'limit' => 16, + 'default' => \Directus\Api\Routes\Webhook::STATUS_INACTIVE + ]); + + $table->addColumn('collection', 'string', [ + 'limit' => 255, + 'null' => true, + 'default' => null + ]); + + $table->addColumn('directus_action', 'string', [ + 'limit' => 255, + 'encoding' => 'utf8', + 'null' => true, + 'default' => null + ]); + + $table->addColumn('url', 'string', [ + 'limit' => 510, + 'encoding' => 'utf8', + 'null' => true, + 'default' => null + ]); + + $table->addColumn('http_action', 'string', [ + 'limit' => 255, + 'encoding' => 'utf8', + 'null' => true, + 'default' => null + ]); + + + + $table->create(); + + // Insert Into Directus Fields + $data = [ + [ + 'collection' => 'directus_webhooks', + 'field' => 'id', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_INTEGER, + 'interface' => 'primary-key', + 'locked' => 1, + 'required' => 1, + 'hidden_detail' => 1 + ], + [ + 'collection' => 'directus_webhooks', + 'field' => 'status', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STATUS, + 'interface' => 'status', + 'options' => json_encode([ + 'status_mapping' => [ + 'published' => [ + 'name' => 'Published', + 'value' => 'published', + 'text_color' => 'white', + 'background_color' => 'accent', + 'browse_subdued' => false, + 'browse_badge' => true, + 'soft_delete' => false, + 'published' => true, + ], + 'draft' => [ + 'name' => 'Draft', + 'value' => 'draft', + 'text_color' => 'white', + 'background_color' => 'blue-grey-100', + 'browse_subdued' => true, + 'browse_badge' => true, + 'soft_delete' => false, + 'published' => false, + ], + 'deleted' => [ + 'name' => 'Deleted', + 'value' => 'deleted', + 'text_color' => 'white', + 'background_color' => 'red', + 'browse_subdued' => true, + 'browse_badge' => true, + 'soft_delete' => true, + 'published' => false, + ] + ] + ]), + 'required' => 1 + ], + [ + 'collection' => 'directus_webhooks', + 'field' => 'http_action', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'dropdown', + 'required' => 1, + 'options' => json_encode([ + 'choices' => [ + 'get' => 'Get', + 'post' => 'Post' + ] + ]) + ], + [ + 'collection' => 'directus_webhooks', + 'field' => 'collection', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'collections', + 'required' => 1 + ], + [ + 'collection' => 'directus_webhooks', + 'field' => 'directus_action', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'dropdown', + 'required' => 1, + 'options' => json_encode([ + 'choices' => [ + 'item.create:before' => 'item.create:before', + 'item.create:after' => 'item.create:after', + 'item.update:before' => 'item.update:before', + 'item.update:after' => 'item.update:after', + 'item.delete:before' => 'item.delete:before', + 'item.delete:after' => 'item.delete:after', + ] + ]) + ], + [ + 'collection' => 'directus_webhooks', + 'field' => 'url', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'interface' => 'text-input', + 'required' => 1 + ] + + ]; + + foreach($data as $value){ + if(!$this->checkFieldExist($value['collection'], $value['field'])){ + $insertSqlFormat = "INSERT INTO `directus_fields` (`collection`, `field`, `type`, `interface`, `hidden_detail`, `required`, `locked`, `options`) VALUES ('%s', '%s', '%s', '%s', '%s', '%s','%s' , '%s');"; + + $insertSql = sprintf($insertSqlFormat,$value['collection'], $value['field'], $value['type'], $value['interface'], isset($value['hidden_detail']) ? $value['hidden_detail'] : 0, $value['required'], isset($value['locked']) ? $value['locked'] : 0, isset($value['options']) ? $value['options'] : null); + $this->execute($insertSql); + } + } + } + + public function checkFieldExist($collection,$field){ + $checkSql = sprintf('SELECT 1 FROM `directus_fields` WHERE `collection` = "%s" AND `field` = "%s";', $collection, $field); + return $this->query($checkSql)->fetch(); + } +} diff --git a/migrations/upgrades/schemas/20191001092213_add_project_settings.php b/migrations/upgrades/schemas/20191001092213_add_project_settings.php new file mode 100644 index 0000000000..ffe9e0005c --- /dev/null +++ b/migrations/upgrades/schemas/20191001092213_add_project_settings.php @@ -0,0 +1,61 @@ +execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'width' => 'half' + ], + ['collection' => 'directus_settings', 'field' => 'file_naming'] + )); + + // Update width of file nameing option + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'width' => 'half' + ], + ['collection' => 'directus_settings', 'field' => 'login_attempts_allowed'] + )); + + + $settings = [ + 'project_icon' => [ + 'interface' => 'icon', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_STRING, + 'width' => 'half', + ], + 'project_image' => [ + 'interface' => 'file', + 'width' => 'half', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_FILE, + ] + ]; + foreach ($settings as $field => $options) { + $this->addField($field, $options); + } + } + + protected function addField($field, $options) + { + $collection = 'directus_settings'; + $checkSql = sprintf('SELECT 1 FROM `directus_fields` WHERE `collection` = "%s" AND `field` = "%s";', $collection, $field); + $result = $this->query($checkSql)->fetch(); + + if (!$result) { + $insertSqlFormat = 'INSERT INTO `directus_fields` (`collection`, `field`, `type`, `interface`,`width`) VALUES ("%s", "%s", "%s", "%s", "%s");'; + $insertSql = sprintf($insertSqlFormat, $collection, $field, $options['type'], $options['interface'], $options['width']); + $this->execute($insertSql); + } + } +} diff --git a/migrations/upgrades/schemas/20191002070945_rename_settings.php b/migrations/upgrades/schemas/20191002070945_rename_settings.php new file mode 100644 index 0000000000..0e3c103d3c --- /dev/null +++ b/migrations/upgrades/schemas/20191002070945_rename_settings.php @@ -0,0 +1,31 @@ +execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'field' => 'project_color' + ], + ['collection' => 'directus_settings', 'field' => 'color'] + )); + + // Update the key in directus_settings collection + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_settings', + [ + 'key' => 'project_color' + ], + ['key' => 'color'] + )); + } +} diff --git a/migrations/upgrades/schemas/20191007113144_update_type_for_uploaded_by_directus_files.php b/migrations/upgrades/schemas/20191007113144_update_type_for_uploaded_by_directus_files.php new file mode 100644 index 0000000000..40847ce4a7 --- /dev/null +++ b/migrations/upgrades/schemas/20191007113144_update_type_for_uploaded_by_directus_files.php @@ -0,0 +1,20 @@ +execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'type' => \Directus\Database\Schema\DataTypes::TYPE_USER_CREATED, + 'interface' => 'user-created' + ], + ['collection' => 'directus_files', 'field' => 'uploaded_by'] + )); + } +} diff --git a/migrations/upgrades/schemas/20191105063515_update_general_setting_variabe_name.php b/migrations/upgrades/schemas/20191105063515_update_general_setting_variabe_name.php new file mode 100644 index 0000000000..695757ccb1 --- /dev/null +++ b/migrations/upgrades/schemas/20191105063515_update_general_setting_variabe_name.php @@ -0,0 +1,100 @@ +execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'field' => 'project_logo' + ], + ['collection' => 'directus_settings', 'field' => 'logo'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_settings', + [ + 'key' => 'project_logo' + ], + ['key' => 'logo'] + )); + + // Update the interface of project_icon from icon to file and rename it. + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'field' => 'project_foreground', + 'type' => \Directus\Database\Schema\DataTypes::TYPE_FILE, + 'interface' => 'file' + ], + ['collection' => 'directus_settings', 'field' => 'project_icon'] + )); + + // Need to delete the project_icon as this migration will change the interface to file and the icon will contain the string + $result = $this->query('SELECT 1 FROM `directus_settings` WHERE `key` = "project_icon";')->fetch(); + + if ($result) { + $this->execute('DELETE FROM `directus_settings` where `key` = "project_icon";'); + } + + // Rename project_image + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'field' => 'project_background' + ], + ['collection' => 'directus_settings', 'field' => 'project_image'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_settings', + [ + 'key' => 'project_background' + ], + ['key' => 'project_image'] + )); + + $result = $this->query('SELECT 1 FROM `directus_fields` WHERE `field` = "app_url";')->fetch(); + + if ($result) { + $this->execute('DELETE FROM `directus_fields` where `field` = "app_url";'); + } + + $result = $this->query('SELECT 1 FROM `directus_settings` WHERE `key` = "app_url";')->fetch(); + + if ($result) { + $this->execute('DELETE FROM `directus_settings` where `key` = "app_url";'); + } + } +} diff --git a/migrations/upgrades/schemas/20191112161523_add_locale_and_telemetry_to_settings_table.php b/migrations/upgrades/schemas/20191112161523_add_locale_and_telemetry_to_settings_table.php new file mode 100644 index 0000000000..04eae23283 --- /dev/null +++ b/migrations/upgrades/schemas/20191112161523_add_locale_and_telemetry_to_settings_table.php @@ -0,0 +1,32 @@ + [ + 'type' => 'string', + 'interface' => 'language' + ], + 'telemetry' => [ + 'type' => 'boolean', + 'interface' => 'toggle' + ] + ]; + foreach ($settings as $field => $options) { + $this->addField($field, $options['type'], $options['interface']); + } + } + protected function addField($field, $type, $interface) + { + $collection = 'directus_settings'; + $checkSql = sprintf('SELECT 1 FROM `directus_fields` WHERE `collection` = "%s" AND `field` = "%s";', $collection, $field); + $result = $this->query($checkSql)->fetch(); + if (!$result) { + $insertSqlFormat = 'INSERT INTO `directus_fields` (`collection`, `field`, `type`, `interface`) VALUES ("%s", "%s", "%s", "%s");'; + $insertSql = sprintf($insertSqlFormat, $collection, $field, $type, $interface); + $this->execute($insertSql); + } + } +} \ No newline at end of file diff --git a/migrations/upgrades/schemas/20191112175432_remove_activity_seen_fields.php b/migrations/upgrades/schemas/20191112175432_remove_activity_seen_fields.php new file mode 100644 index 0000000000..5ed8e97d47 --- /dev/null +++ b/migrations/upgrades/schemas/20191112175432_remove_activity_seen_fields.php @@ -0,0 +1,48 @@ +query('SELECT 1 FROM `directus_fields` WHERE `collection` = "directus_activity_seen"')->fetch(); + + if ($result) { + $this->execute('DELETE FROM `directus_fields` WHERE `collection` = "directus_activity_seen"'); + } + + $result = $this->query('SELECT 1 FROM `directus_relations` WHERE `collection_many` = "directus_activity_seen"')->fetch(); + + if ($result) { + $this->execute('DELETE FROM `directus_relations` WHERE `collection_many` = "directus_activity_seen"'); + } + + + $this->execute('DROP TABLE IF EXISTS directus_activity_seen'); + + } +} diff --git a/migrations/upgrades/schemas/20191113083711_update_current_migrations.php b/migrations/upgrades/schemas/20191113083711_update_current_migrations.php new file mode 100644 index 0000000000..7208dde547 --- /dev/null +++ b/migrations/upgrades/schemas/20191113083711_update_current_migrations.php @@ -0,0 +1,640 @@ +execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'change_history' + ]), + 'width' => 'full' + ], + ['collection' => 'directus_activity', 'field' => 'action'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'list_alt' + ]) + ], + ['collection' => 'directus_activity', 'field' => 'collection'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'link' + ]) + ], + ['collection' => 'directus_activity', 'field' => 'item'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'account_circle' + ]) + ], + ['collection' => 'directus_activity', 'field' => 'action_by'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'showRelative' => true, + 'iconRight' => 'calendar_today' + ]) + ], + ['collection' => 'directus_activity', 'field' => 'action_on'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'showRelative' => true, + 'iconRight' => 'edit' + ]) + ], + ['collection' => 'directus_activity', 'field' => 'edited_on'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'showRelative' => true, + 'iconRight' => 'delete_outline' + ]) + ], + ['collection' => 'directus_activity', 'field' => 'comment_deleted_on'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'my_location' + ]) + ], + ['collection' => 'directus_activity', 'field' => 'ip'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'devices_other' + ]), + 'width' => 'full' + ], + ['collection' => 'directus_activity', 'field' => 'user_agent'] + )); + + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'placeholder' => 'Enter a keyword then hit enter...' + ]), + ], + ['collection' => 'directus_files', 'field' => 'tags'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'toolbar' => ['bold','italic','underline','link','code'] + ]), + ], + ['collection' => 'directus_files', 'field' => 'description'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'width' => 'full' + ], + ['collection' => 'directus_files', 'field' => 'metadata'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'title' + ]), + 'width' => 'half', + 'note' => 'Logo in the top-left of the App (40x40)', + ], + ['collection' => 'directus_settings', 'field' => 'project_name'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'note' => 'A 40x40 brand logo, ideally a white SVG/PNG', + ], + ['collection' => 'directus_settings', 'field' => 'project_logo'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'keyboard_tab' + ]), + 'note' => 'Default item count in API and App responses', + 'sort' => 11 + ], + ['collection' => 'directus_settings', 'field' => 'default_limit'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'note' => 'NULL values are sorted last', + ], + ['collection' => 'directus_settings', 'field' => 'sort_null_last'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'timer' + ]), + 'note' => 'Minutes before idle users are signed out', + 'sort' => 22 + ], + ['collection' => 'directus_settings', 'field' => 'auto_sign_out'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'placeholder' => 'Allowed dimensions for thumbnails (eg: 200x200)' + ]), + 'width' => 'full', + 'sort' => 34 + ], + ['collection' => 'directus_settings', 'field' => 'thumbnail_dimensions'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'note' => 'Allowed qualities for thumbnails', + 'sort' => 35 + ], + ['collection' => 'directus_settings', 'field' => 'thumbnail_quality_tags'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'note' => 'Defines how the thumbnail will be generated based on the requested dimensions', + 'sort' => 36 + ], + ['collection' => 'directus_settings', 'field' => 'thumbnail_actions'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'broken_image' + ]), + 'locked' => 1, + 'width' => 'full', + 'note' => 'A fallback image used when thumbnail generation fails', + 'sort' => 37 + ], + ['collection' => 'directus_settings', 'field' => 'thumbnail_not_found_location'] + )); + + $result = $this->query('SELECT 1 FROM `directus_fields` WHERE `collection` = "directus_settings" and `field` = "thumbnail_cache_ttl";')->fetch(); + + if ($result) { + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'cached' + ]), + 'required' => 1, + 'note' => 'Seconds before browsers re-fetch thumbnails', + 'sort' => 38 + ], + ['collection' => 'directus_settings', 'field' => 'thumbnail_cache_ttl'] + )); + } + + $result = $this->query('SELECT 1 FROM `directus_fields` WHERE `collection` = "directus_settings" and `field` = "youtube_api";')->fetch(); + + if ($result) { + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'videocam' + ]), + 'width' => 'half', + 'note' => 'Allows fetching more YouTube Embed info', + 'sort' => 39 + ], + ['collection' => 'directus_settings', 'field' => 'youtube_api'] + )); + } + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'required' => 0 + ], + ['collection' => 'directus_users', 'field' => 'locale'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'account_circle' + ]), + ], + ['collection' => 'directus_users', 'field' => 'first_name'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'account_circle' + ]), + ], + ['collection' => 'directus_users', 'field' => 'last_name'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'alternate_email' + ]), + ], + ['collection' => 'directus_users', 'field' => 'email'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'location_city' + ]), + ], + ['collection' => 'directus_users', 'field' => 'company'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'iconRight' => 'text_fields' + ]), + ], + ['collection' => 'directus_users', 'field' => 'title'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'sort' => 15 + ], + ['collection' => 'directus_users', 'field' => 'locale_options'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'sort' => 16 + ], + ['collection' => 'directus_users', 'field' => 'token'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'sort' => 17 + ], + ['collection' => 'directus_users', 'field' => 'last_access_on'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'sort' => 18 + ], + ['collection' => 'directus_users', 'field' => 'last_page'] + )); + + + $result = $this->query('SELECT 1 FROM `directus_fields` WHERE `collection` = "directus_users" and `field` = "last_login";')->fetch(); + + if ($result) { + $this->execute('DELETE FROM `directus_fields` where `collection` = "directus_users" and `field` = "last_login";'); + $this->execute('ALTER TABLE `directus_users` DROP last_login;'); + } + + $result = $this->query('SELECT 1 FROM `directus_fields` WHERE `collection` = "directus_users" and `field` = "invite_token";')->fetch(); + + if ($result) { + $this->execute('DELETE FROM `directus_fields` where `collection` = "directus_users" and `field` = "invite_token";'); + $this->execute('ALTER TABLE `directus_users` DROP `invite_token`;'); + } + + $result = $this->query('SELECT 1 FROM `directus_fields` WHERE `collection` = "directus_users" and `field` = "invite_accepted";')->fetch(); + + if ($result) { + $this->execute('DELETE FROM `directus_fields` where `collection` = "directus_users" and `field` = "invite_accepted";'); + $this->execute('ALTER TABLE `directus_users` DROP `invite_accepted`;'); + } + + + $result = $this->query('SELECT 1 FROM `directus_fields` WHERE `collection` = "directus_users" and `field` = "last_ip";')->fetch(); + + if ($result) { + $this->execute('DELETE FROM `directus_fields` where `collection` = "directus_users" and `field` = "last_ip";'); + $this->execute('ALTER TABLE `directus_users` DROP `last_ip`;'); + } + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'limit' => true + ]), + 'width' => 'half', + 'note' => 'Default locale for Directus Users', + 'sort' => 7 + ], + ['collection' => 'directus_settings', 'field' => 'default_locale'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'hidden_browse' => 1, + ], + ['collection' => 'directus_settings', 'field' => 'data_divider'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'hidden_browse' => 1, + ], + ['collection' => 'directus_settings', 'field' => 'security_divider'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'hidden_browse' => 1, + ], + ['collection' => 'directus_settings', 'field' => 'files_divider'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'hidden_browse' => 1, + ], + ['collection' => 'directus_webhooks', 'field' => 'info'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'width' => 'half', + 'note' => 'Learn More', + 'sort' => 8 + ], + ['collection' => 'directus_settings', 'field' => 'telemetry'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'locked' => 1, + 'width' => 'full', + 'sort' => 1 + ], + ['collection' => 'directus_webhooks', 'field' => 'status'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'locked' => 1, + 'width' => 'half-space', + 'sort' => 2 + ], + ['collection' => 'directus_webhooks', 'field' => 'http_action'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'placeholder' => 'https://example.com', + 'iconRight' => 'link' + ]), + 'locked' => 1, + 'width' => 'full', + 'note' => '', + 'sort' => 3 + ], + ['collection' => 'directus_webhooks', 'field' => 'url'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'interface' => 'collections', + 'locked' => 1, + 'width' => 'half', + 'note' => '', + 'sort' => 4 + ], + ['collection' => 'directus_webhooks', 'field' => 'collection'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'locked' => 1, + 'width' => 'half', + 'note' => '', + 'sort' => 5 + ], + ['collection' => 'directus_webhooks', 'field' => 'directus_action'] + )); + + $result = $this->query('SELECT 1 FROM `directus_fields` WHERE `collection` = "directus_webhooks" and `field` = "info";')->fetch(); + + if (!$result) { + $options = json_encode([ + 'style' => 'medium', + 'title' => 'How Webhooks Work', + 'hr' => true, + 'margin' => false, + 'description' => 'When the selected action occurs for the selected collection, Directus will send an HTTP request to the above URL.' + ]); + + $this->execute("INSERT INTO `directus_fields` (`collection`, `field`, `type`, `interface`,`options`, `locked`, `width`, `sort`) VALUES ('directus_webhooks', 'info', '".\Directus\Database\Schema\DataTypes::TYPE_ALIAS."', 'divider', '".$options."', '1', 'full', '6');"); + } + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'status_mapping' => [ + 'active' => [ + 'name' => 'Active', + 'value' => 'active', + 'text_color' => 'white', + 'background_color' => 'green', + 'browse_subdued' => false, + 'browse_badge' => true, + 'soft_delete' => false, + 'published' => true, + ], + 'inactive' => [ + 'name' => 'Inactive', + 'value' => 'inactive', + 'text_color' => 'white', + 'background_color' => 'blue-grey', + 'browse_subdued' => true, + 'browse_badge' => true, + 'soft_delete' => false, + 'published' => false, + ] + ] + ]), + ], + ['collection' => 'directus_webhooks', 'field' => 'status'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'choices' => [ + 'get' => 'GET', + 'post' => 'POST' + ] + ]), + ], + ['collection' => 'directus_webhooks', 'field' => 'http_action'] + )); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + [ + 'options' => json_encode([ + 'choices' => [ + 'item.create:after' => 'Create', + 'item.update:after' => 'Update', + 'item.delete:after' => 'Delete', + ] + ]), + ], + ['collection' => 'directus_webhooks', 'field' => 'directus_action'] + )); + + + // Role table + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_roles', + [ + 'description' => 'Controls what API data is publicly available without authenticating' + ], + ['name' => 'public'] + )); + + // Settings Table + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_settings', + [ + 'value' => '10080' + ], + ['key' => 'auto_sign_out'] + )); + + + } +} diff --git a/public/extensions/core/auth/facebook/icon.svg b/public/extensions/core/auth/facebook/icon.svg index ea68af35e8..d7b4d7b514 100644 --- a/public/extensions/core/auth/facebook/icon.svg +++ b/public/extensions/core/auth/facebook/icon.svg @@ -1 +1 @@ - + diff --git a/public/extensions/core/auth/github/icon.svg b/public/extensions/core/auth/github/icon.svg index 80681e7430..374ce2de33 100644 --- a/public/extensions/core/auth/github/icon.svg +++ b/public/extensions/core/auth/github/icon.svg @@ -1 +1 @@ - + diff --git a/public/extensions/core/auth/google/icon.svg b/public/extensions/core/auth/google/icon.svg index a588b55a0b..ad90bed9c3 100644 --- a/public/extensions/core/auth/google/icon.svg +++ b/public/extensions/core/auth/google/icon.svg @@ -1 +1 @@ - + diff --git a/public/extensions/core/auth/okta/icon.svg b/public/extensions/core/auth/okta/icon.svg index 25ad3413af..1e911f2db8 100644 --- a/public/extensions/core/auth/okta/icon.svg +++ b/public/extensions/core/auth/okta/icon.svg @@ -1 +1 @@ - + diff --git a/public/extensions/core/auth/twitter/icon.svg b/public/extensions/core/auth/twitter/icon.svg index a897a339c7..220ea5180d 100644 --- a/public/extensions/core/auth/twitter/icon.svg +++ b/public/extensions/core/auth/twitter/icon.svg @@ -1 +1 @@ - + diff --git a/src/core/Directus/Application/Application.php b/src/core/Directus/Application/Application.php index 63d7c3bcf6..9b073d563c 100644 --- a/src/core/Directus/Application/Application.php +++ b/src/core/Directus/Application/Application.php @@ -13,7 +13,7 @@ class Application extends App * * @var string */ - const DIRECTUS_VERSION = '2.6.0'; + const DIRECTUS_VERSION = '8.0.0-rc.1'; /** * NOT USED diff --git a/src/core/Directus/Application/CoreServicesProvider.php b/src/core/Directus/Application/CoreServicesProvider.php index 525f3b3285..d8e05abacd 100644 --- a/src/core/Directus/Application/CoreServicesProvider.php +++ b/src/core/Directus/Application/CoreServicesProvider.php @@ -850,7 +850,7 @@ protected function getExternalAuth() $socialAuth->register($providerName, new $class($container, array_merge([ 'custom' => $custom, - 'callback_url' => \Directus\get_url('/_/auth/sso/' . $providerName . '/callback') + 'callback_url' => \Directus\get_url('/'.get_api_project_from_request().'/auth/sso/' . $providerName . '/callback') ], $providerConfig))); } } diff --git a/src/core/Directus/Application/ErrorHandlers/ErrorHandler.php b/src/core/Directus/Application/ErrorHandlers/ErrorHandler.php index b3e06d90c4..941a144afc 100644 --- a/src/core/Directus/Application/ErrorHandlers/ErrorHandler.php +++ b/src/core/Directus/Application/ErrorHandlers/ErrorHandler.php @@ -69,7 +69,7 @@ public function __invoke(Request $request, Response $response, $exception) $data = $this->processException($exception); $response = $response->withStatus($data['http_status_code']); - + $this->triggerResponseAction($request, $response, $data); if ($this->isMessageSCIM($response)) { @@ -82,7 +82,8 @@ public function __invoke(Request $request, Response $response, $exception) } return $response - ->withJson(['error' => $data['error']]); + ->withJson(['error' => $data['error']]) + ->withHeader('Access-Control-Allow-Origin', $request->getHeader('Origin')); } /** diff --git a/src/core/Directus/Application/Http/Middleware/AuthenticationMiddleware.php b/src/core/Directus/Application/Http/Middleware/AuthenticationMiddleware.php index 2a0788ba24..937f701ab2 100644 --- a/src/core/Directus/Application/Http/Middleware/AuthenticationMiddleware.php +++ b/src/core/Directus/Application/Http/Middleware/AuthenticationMiddleware.php @@ -11,6 +11,7 @@ use Directus\Database\TableGateway\DirectusPermissionsTableGateway; use Directus\Exception\UnauthorizedLocationException; use function Directus\get_request_authorization_token; +use function Directus\get_static_token_based_on_type; use Directus\Permissions\Acl; use Directus\Services\AuthService; use Directus\Services\UsersService; @@ -48,7 +49,6 @@ public function __invoke(Request $request, Response $response, callable $next) $user = $this->authenticate($request); $hookEmitter = $this->container->get('hook_emitter'); - if (!$user && !$publicRoleId) { $exception = new UserNotAuthenticatedException(); $hookEmitter->run('auth.fail', [$exception]); @@ -110,7 +110,7 @@ public function __invoke(Request $request, Response $response, callable $next) $hookEmitter->run('auth.fail', [$exception]); throw $exception; } - + // TODO: Adding an user should auto set its ID and GROUP // TODO: User data should be casted to its data type // TODO: Make sure that the group is not empty @@ -152,7 +152,8 @@ protected function authenticate(Request $request) */ protected function getAuthToken(Request $request) { - return get_request_authorization_token($request); + $authToken = get_request_authorization_token($request); + return get_static_token_based_on_type($authToken); } /** @@ -225,8 +226,11 @@ protected function targetIsUserEdit(Request $request, int $id) { if ($num_elements > 3 &&$target_array[$num_elements - 3] == 'users' - && $target_array[$num_elements - 2] == strval($id) - && $target_array[$num_elements - 1] == 'activate2FA') { + && ( + $target_array[$num_elements - 2] == strval($id) || + $target_array[$num_elements - 2] == 'me' + ) + && $target_array[$num_elements - 1] == 'activate_2fa') { return true; } diff --git a/src/core/Directus/Application/Http/Middleware/CorsMiddleware.php b/src/core/Directus/Application/Http/Middleware/CorsMiddleware.php index 780c47c30c..dbb9bc5614 100644 --- a/src/core/Directus/Application/Http/Middleware/CorsMiddleware.php +++ b/src/core/Directus/Application/Http/Middleware/CorsMiddleware.php @@ -57,7 +57,12 @@ public function __invoke(Request $request, Response $response, callable $next) if ($this->isEnabled()) { if ($request->isOptions()) { $this->processPreflightHeaders($request, $response); - return $response; + + // These withHeader calls are a temporary hack to get around the $response here not containig the correct headers that are supposedly set above in the processpreflightheaders call. + // TODO: Remove withHeaders calls here + return $response + ->withHeader('Access-Control-Allow-Credentials', 'true') + ->withHeader('Access-Control-Allow-Headers', ['X-Directus-Project', 'Content-Type', 'Authorization']); } else { $this->processActualHeaders($request, $response); } diff --git a/src/core/Directus/Application/Http/Middleware/DatabaseMigrationMiddleware.php b/src/core/Directus/Application/Http/Middleware/DatabaseMigrationMiddleware.php new file mode 100644 index 0000000000..b2c6bba7ff --- /dev/null +++ b/src/core/Directus/Application/Http/Middleware/DatabaseMigrationMiddleware.php @@ -0,0 +1,17 @@ +container->get('path_base'), \Directus\get_api_project_from_request()); + + return $next($request, $response); + } +} diff --git a/src/core/Directus/Application/Http/Middleware/ResponseCacheMiddleware.php b/src/core/Directus/Application/Http/Middleware/ResponseCacheMiddleware.php index 1a4852a999..c309b84bdd 100644 --- a/src/core/Directus/Application/Http/Middleware/ResponseCacheMiddleware.php +++ b/src/core/Directus/Application/Http/Middleware/ResponseCacheMiddleware.php @@ -6,6 +6,14 @@ use Directus\Application\Http\Request; use Directus\Application\Http\Response; use Directus\Util\StringUtils; +use Slim\Http\Cookies; +use function Directus\get_directus_setting; +use function Directus\decrypt_static_token; +use function Directus\get_project_session_cookie_name; +use function Directus\get_request_authorization_token; +use Directus\Services\UserSessionService; +use Directus\Database\TableGateway\DirectusUserSessionsTableGateway; +use Directus\Util\DateTimeUtils; class ResponseCacheMiddleware extends AbstractMiddleware { @@ -52,7 +60,6 @@ public function __invoke(Request $request, Response $response, callable $next) } else { /** @var Response $response */ $response = $next($request, $response); - $body = $response->getBody(); $body->rewind(); $bodyContent = $body->getContents(); @@ -61,6 +68,48 @@ public function __invoke(Request $request, Response $response, callable $next) $cache->process($key, $bodyContent, $headers); } + $authorizationTokenObject = get_request_authorization_token($request); + + $accessToken = null; + if(!empty($authorizationTokenObject['token'])){ + $userSessionService = new UserSessionService($container); + $userSessionService->destroy([ + 'token_expired_at < ?' => DateTimeUtils::now()->toString() + ]); + $expirationMinutes = get_directus_setting('auto_sign_out'); + $expiry = new \DateTimeImmutable('now + '.$expirationMinutes.'minutes'); + + switch($authorizationTokenObject['type']){ + case DirectusUserSessionsTableGateway::TOKEN_COOKIE : + $accessToken = decrypt_static_token($authorizationTokenObject['token']); + $userSession = $userSessionService->find(['token' => $accessToken]); + $cookie = new Cookies(); + $expiryAt = $userSession ? $expiry->format(\DateTime::COOKIE) : DateTimeUtils::now()->toString(); + $cookie->set( + get_project_session_cookie_name($request), + [ + 'value' => $authorizationTokenObject['token'], + 'expires' => $expiryAt, + 'path'=>'/', + 'httponly' => true + ] + ); + + $response = $response->withAddedHeader('Set-Cookie',$cookie->toHeaders()); + break; + default : + $userSession = $userSessionService->find(['token' => $authorizationTokenObject['token']]); + break; + } + } + if(isset($userSession)){ + $userSessionService->update($userSession['id'],['token_expired_at' => $expiry->format('Y-m-d H:i:s')]); + } + $response = $response->withHeader('Access-Control-Allow-Origin', $request->getHeader('Origin')); + $config = $container->get('config'); + if ($config->get('cors.credentials')) { + $response = $response->withHeader('Access-Control-Allow-Credentials', 'true'); + } return $response; } } diff --git a/src/core/Directus/Authentication/Exception/SsoNotAllowedException.php b/src/core/Directus/Authentication/Exception/SsoNotAllowedException.php new file mode 100644 index 0000000000..f8f0e4915a --- /dev/null +++ b/src/core/Directus/Authentication/Exception/SsoNotAllowedException.php @@ -0,0 +1,15 @@ + (int) $user->getId(), @@ -438,9 +438,6 @@ public function generateAuthToken(UserInterface $user, $needs2FA = false) 'exp' => $this->getNewExpirationTime() ]; - if ($needs2FA == true) { - $payload['needs2FA'] = true; - } return $this->generateToken(JWTUtils::TYPE_AUTH, $payload); } @@ -511,7 +508,7 @@ public function generateToken($type, array $payload) $payload['key'] = $this->getPublicKey(); $payload['project'] = get_api_project_from_request(); - return JWTUtils::encode($payload, $this->getSecretKey(), $this->getTokenAlgorithm()); + return JWTUtils::encode($payload, $this->getSecretKey($payload['project']), $this->getTokenAlgorithm()); } /** @@ -536,7 +533,7 @@ public function refreshToken($token, $needs2FA = false) $payload->needs2FA = $needs2FA; - return JWTUtils::encode($payload, $this->getSecretKey(), $this->getTokenAlgorithm()); + return JWTUtils::encode($payload, $this->getSecretKey(get_api_project_from_request()), $this->getTokenAlgorithm()); } /** @@ -609,7 +606,7 @@ public function hashPassword($password) * * @return string */ - public function getSecretKey($project = '_') + public function getSecretKey($project) { if ($project) { $config = get_project_config($project); diff --git a/src/core/Directus/Config/Context.php b/src/core/Directus/Config/Context.php index 1a3a7906ad..93a9e08608 100644 --- a/src/core/Directus/Config/Context.php +++ b/src/core/Directus/Config/Context.php @@ -36,6 +36,9 @@ private static function expand(&$target, $path, $value) if (!isset($target[$segment])) { $target[$segment] = []; } + if (!is_array($target[$segment])) { + $target[$segment] = []; + } Context::expand($target[$segment], $path, $value); } diff --git a/src/core/Directus/Config/Exception/UnknownProjectException.php b/src/core/Directus/Config/Exception/UnknownProjectException.php index c703e33932..b8b8e69729 100644 --- a/src/core/Directus/Config/Exception/UnknownProjectException.php +++ b/src/core/Directus/Config/Exception/UnknownProjectException.php @@ -6,7 +6,7 @@ class UnknownProjectException extends ErrorException { - const ERROR_CODE = 22; + const ERROR_CODE = 24; public function __construct($project, $previous = null) { diff --git a/src/core/Directus/Config/Schema/Schema.php b/src/core/Directus/Config/Schema/Schema.php index a1fd43ba13..3b4c27537f 100644 --- a/src/core/Directus/Config/Schema/Schema.php +++ b/src/core/Directus/Config/Schema/Schema.php @@ -38,6 +38,7 @@ public static function get() { new Value('engine', Types::STRING, 'InnoDB'), new Value('charset', Types::STRING, 'utf8mb4'), new Value('socket', Types::STRING, ''), + new Value('driver_options?', Types::ARRAY, []), ]), new Group('cache', [ new Value('enabled', Types::BOOLEAN, false), @@ -54,6 +55,7 @@ public static function get() { new Value('root', Types::STRING, 'public/uploads/_/originals'), new Value('root_url', Types::STRING, '/uploads/_/originals'), new Value('thumb_root', Types::STRING, 'public/uploads/_/thumbnails'), + new Value('proxy_downloads?', Types::BOOLEAN, false), // S3 new Value('key?', Types::STRING, 's3-key'), @@ -67,6 +69,12 @@ public static function get() { new Value('Cache-Control', Types::STRING, 'max-age=604800') ]), + // OSS + new Value('OSS_ACCESS_ID?', Types::STRING, 'oss-access-id'), + new Value('OSS_ACCESS_KEY?', Types::STRING, 'oss-access-secret'), + new Value('OSS_BUCKET?', Types::STRING, 'oss-bucket'), + new Value('OSS_ENDPOINT?', Types::STRING, 'oss-endpoint'), + // TODO: Missing keys? ]), new Group('mail', [ diff --git a/src/core/Directus/Console/Common/Setting.php b/src/core/Directus/Console/Common/Setting.php index a1515d95dc..a17a5db970 100644 --- a/src/core/Directus/Console/Common/Setting.php +++ b/src/core/Directus/Console/Common/Setting.php @@ -21,7 +21,7 @@ public function __construct($base_path, $projectName = null) $this->directus_path = $base_path; - $app = InstallerUtils::createApp($base_path, $config); + $app = InstallerUtils::createApp($base_path, $projectName); $this->db = $app->getContainer()->get('database'); $this->settingsTableGateway = new TableGateway('directus_settings', $this->db); diff --git a/src/core/Directus/Database/Schema/DataTypes.php b/src/core/Directus/Database/Schema/DataTypes.php index 18825501a3..490a7ac8cf 100644 --- a/src/core/Directus/Database/Schema/DataTypes.php +++ b/src/core/Directus/Database/Schema/DataTypes.php @@ -327,6 +327,18 @@ public static function getSystemDateTimeTypes() static::TYPE_DATETIME_UPDATED ]; } + /** + * Returns all the system user types + * + * @return array + */ + public static function getSystemUserType() + { + return [ + static::TYPE_USER_CREATED, + static::TYPE_USER_UPDATED + ]; + } /** * Checks whether or not the given type is system datetime type @@ -340,6 +352,18 @@ public static function isSystemDateTimeType($type) return in_array(strtolower($type), static::getSystemDateTimeTypes()); } + /** + * Checks whether or not the given type is system user type + * + * @param string $type + * + * @return bool + */ + public static function isSystemUserType($type) + { + return in_array(strtolower($type), static::getSystemUserType()); + } + /** * Checks whether or not the given type is status type * diff --git a/src/core/Directus/Database/Schema/Object/Field.php b/src/core/Directus/Database/Schema/Object/Field.php index cae620b9ae..9e2f20e1a1 100644 --- a/src/core/Directus/Database/Schema/Object/Field.php +++ b/src/core/Directus/Database/Schema/Object/Field.php @@ -332,6 +332,16 @@ public function isSystemDateTimeType() return DataTypes::isSystemDateTimeType($this->getType()); } + /** + * Checks whether this column is system user interface + * + * @return bool + */ + public function isSystemUserType() + { + return DataTypes::isSystemUserType($this->getType()); + } + /** * Checks whether or not the field is a status type * diff --git a/src/core/Directus/Database/Schema/SchemaManager.php b/src/core/Directus/Database/Schema/SchemaManager.php index 57f306a350..9ef69b4cfa 100644 --- a/src/core/Directus/Database/Schema/SchemaManager.php +++ b/src/core/Directus/Database/Schema/SchemaManager.php @@ -15,7 +15,6 @@ class SchemaManager { // Tables const COLLECTION_ACTIVITY = 'directus_activity'; - const COLLECTION_ACTIVITY_SEEN = 'directus_activity_seen'; const COLLECTION_COLLECTIONS = 'directus_collections'; const COLLECTION_COLLECTION_PRESETS = 'directus_collection_presets'; const COLLECTION_FIELDS = 'directus_fields'; @@ -29,6 +28,8 @@ class SchemaManager const COLLECTION_SETTINGS = 'directus_settings'; const COLLECTION_USER_ROLES = 'directus_user_roles'; const COLLECTION_USERS = 'directus_users'; + const COLLECTION_WEBHOOKS = 'directus_webhooks'; + const COLLECTION_USER_SESSIONS = 'directus_user_sessions'; /** * Schema source instance @@ -492,7 +493,6 @@ public static function getSystemCollections() { return [ static::COLLECTION_ACTIVITY, - static::COLLECTION_ACTIVITY_SEEN, static::COLLECTION_COLLECTIONS, static::COLLECTION_COLLECTION_PRESETS, static::COLLECTION_FIELDS, @@ -789,7 +789,7 @@ protected function addCollection($name, $schema) // save the column into the data // @NOTE: this is the early implementation of cache // soon this will be change to cache - $this->data['tables'][$name] = $schema; + $this->data['collections'][$name] = $schema; } protected function addField(Field $column) diff --git a/src/core/Directus/Database/TableGateway/BaseTableGateway.php b/src/core/Directus/Database/TableGateway/BaseTableGateway.php index df39de568e..7a769d59c4 100644 --- a/src/core/Directus/Database/TableGateway/BaseTableGateway.php +++ b/src/core/Directus/Database/TableGateway/BaseTableGateway.php @@ -534,6 +534,12 @@ public function stopManaging($tableName = null) 'collection' => $tableName ]); + // Remove entries from directus_relations + $columnsTableGateway = new TableGateway(SchemaManager::COLLECTION_RELATIONS, $this->adapter); + $columnsTableGateway->delete([ + 'collection_many' => $tableName + ]); + // Remove table from directus_tables $tablesTableGateway = new TableGateway(SchemaManager::COLLECTION_COLLECTIONS, $this->adapter); $tablesTableGateway->delete([ @@ -879,10 +885,12 @@ protected function executeUpdate(Update $update) } //Invalidate individual cache - $config = static::$container->get('config'); - if ($config->get('cache.enabled')) { - $cachePool = static::$container->get('cache'); - $cachePool->invalidateTags(['entity_' . $updateTable . '_' . $result[$this->primaryKeyFieldName]]); + if (static::$container) { + $config = static::$container->get('config'); + if ($config->get('cache.enabled')) { + $cachePool = static::$container->get('cache'); + $cachePool->invalidateTags(['entity_' . $updateTable . '_' . $result[$this->primaryKeyFieldName]]); + } } return $result; @@ -942,15 +950,16 @@ protected function executeDelete(Delete $delete) //Invalidate individual cache - $config = static::$container->get('config'); - + if (static::$container) { + $config = static::$container->get('config'); + } foreach ($ids as $id) { $deleteData = $deletedObject[$id]; $this->runHook('item.delete', [$deleteTable, $deleteData]); $this->runHook('item.delete:after', [$deleteTable, $deleteData]); $this->runHook('item.delete.' . $deleteTable, [$deleteData]); $this->runHook('item.delete.' . $deleteTable . ':after', [$deleteData]); - if ($config->get('cache.enabled')) { + if (isset($config) && $config->get('cache.enabled')) { $cachePool = static::$container->get('cache'); $cachePool->invalidateTags(['entity_' . $deleteTable . '_' . $deleteData[$this->primaryKeyFieldName]]); } diff --git a/src/core/Directus/Database/TableGateway/DirectusUserSessionsTableGateway.php b/src/core/Directus/Database/TableGateway/DirectusUserSessionsTableGateway.php new file mode 100644 index 0000000000..8f150a1ddf --- /dev/null +++ b/src/core/Directus/Database/TableGateway/DirectusUserSessionsTableGateway.php @@ -0,0 +1,71 @@ + $data['user'], + 'token' => $data['token'], + 'token_type' => $data['token_type'], + 'token_expired_at' => $data['token_expired_at'], + 'created_on' => DateTimeUtils::now()->toString(), + 'ip_address' => \Directus\get_request_ip(), + 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '' + ]; + + $insert = new Insert($this->getTable()); + $insert + ->values($sessionData); + + $this->insertWith($insert); + return $this->getLastInsertValue(); + } + + public function updateSession($id,$data) + { + $update = new Update($this->getTable()); + $update->set($data); + $update->where([ + 'id' => $id + ]); + $this->updateWith($update); + } + + public function fetchSession($condition) + { + $select = new Select($this->getTable()); + $select->columns(['*']); + $select->where($condition); + $select->limit(1); + return $this->selectWith($select)->current(); + } + +} diff --git a/src/core/Directus/Database/TableGateway/RelationalTableGateway.php b/src/core/Directus/Database/TableGateway/RelationalTableGateway.php index 837a38a0a1..78df83709e 100644 --- a/src/core/Directus/Database/TableGateway/RelationalTableGateway.php +++ b/src/core/Directus/Database/TableGateway/RelationalTableGateway.php @@ -658,6 +658,11 @@ public function addOrUpdateToManyRelationships($schema, $parentRow, &$childLogEn $foreignTableName = $relationship->getCollectionMany(); $foreignJoinColumn = $relationship->getFieldMany(); + // we need to store all the deleted items ids + // so we can use it to compare against items that were deleted in previous iterations. + // if we don't check against already deleted IDs, then the recursion will re-create the deleted item. + static $hasBeenDeletedIds = []; + $ForeignTable = new RelationalTableGateway($foreignTableName, $this->adapter, $this->acl); foreach ($foreignDataSet as &$foreignRecord) { if (empty($foreignRecord)) { @@ -669,11 +674,24 @@ public function addOrUpdateToManyRelationships($schema, $parentRow, &$childLogEn // due to our basic "cache" implementation on schema layer $hasPrimaryKey = isset($foreignRecord[$ForeignTable->primaryKeyFieldName]); + // check if this foreignRecord was already deleted from a previous recursive iterations. + $foreignTableHasBeenDeletedIds = \Directus\array_get($hasBeenDeletedIds, $ForeignTable->getTable()); + if($hasPrimaryKey && !empty($foreignTableHasBeenDeletedIds)) { + $id = $foreignRecord[$ForeignTable->primaryKeyFieldName]; + + // skip if already deleted + // otherwise, it will re-create the deleted item/record + if(in_array($id, $foreignTableHasBeenDeletedIds)) + continue; + } + if ($hasPrimaryKey && ArrayUtils::get($foreignRecord, $this->deleteFlag) === true) { $Where = new Where(); $Where->equalTo($ForeignTable->primaryKeyFieldName, $foreignRecord[$ForeignTable->primaryKeyFieldName]); $ForeignTable->delete($Where); + $hasBeenDeletedIds[$ForeignTable->getTable()][] = $foreignRecord[$ForeignTable->primaryKeyFieldName]; + continue; } @@ -771,6 +789,11 @@ public function applyDefaultEntriesSelectParams(array $params) $params['status'] = $statusList; } + // If page is defined as param then add offset dynamically. + if (!isset($params['offset']) && isset($params['page']) && isset($params['limit'])) { + $params['offset'] = $params['limit'] * ($params['page'] - 1); + } + $params = array_merge($defaultParams, $params); if (ArrayUtils::get($params, 'sort')) { @@ -1005,7 +1028,7 @@ public function createEntriesMetadata(array $entries, array $list = []) } if (in_array('filter_count', $list) || in_array('page', $list)) { - $metadata = $this->createMetadataPagination($metadata, $_GET,$countedData); + $metadata = $this->createMetadataPagination($metadata, $this::$container->get('request')->getQueryParams(),$countedData); } return $metadata; @@ -1021,7 +1044,7 @@ public function createEntriesMetadata(array $entries, array $list = []) */ public function createMetadataPagination(array $metadata = [], array $params = [], array $countedData = []) { - if (empty($params)) $params = $_GET; + if (empty($params)) $params = $this::$container->get('request')->getQueryParams(); $filtered = ArrayUtils::get($params, 'filter') || ArrayUtils::get($params, 'q'); @@ -2255,21 +2278,31 @@ public function loadManyToOneRelationships($entries, $columns, array $params = [ } // Replace foreign keys with foreign rows - foreach ($entries as &$parentRow) { + foreach ($entries as $key => &$parentRow) { if (array_key_exists($relationalColumnName, $parentRow)) { // @NOTE: Not always will be a integer // @NOTE: But what about UUIDS and slugs? $foreign_id = (string)$parentRow[$relationalColumnName]; $parentRow[$relationalColumnName] = null; + + // if the foreign_key is empty, then there's nothing more to do + if(empty($foreign_id)) + continue; + // "Did we retrieve the foreign row with this foreign ID in our recent query of the foreign table"? if (array_key_exists($foreign_id, $relatedEntries)) { $parentRow[$relationalColumnName] = $relatedEntries[$foreign_id]; } + else{ + // when foreign_id is not empty but there's no $relatedEntries, + // then it means it was soft-deleted. + unset($entries[$key]); + } } } } - return $entries; + return array_values($entries); } /** diff --git a/src/core/Directus/GraphQL/FieldsConfig.php b/src/core/Directus/GraphQL/FieldsConfig.php index 4c3ee7058c..524654c573 100644 --- a/src/core/Directus/GraphQL/FieldsConfig.php +++ b/src/core/Directus/GraphQL/FieldsConfig.php @@ -66,9 +66,9 @@ public function getFields() $relation = $this->getRelation('o2m', $v['collection'], $v['field']); $temp = []; $temp['type'] = Types::listOf(Types::userCollection($relation['collection_one'])); - $temp['resolve'] = function ($value) use ($relation) { + $temp['resolve'] = function ($value, $args, $context, $info) use ($relation) { $data = []; - foreach ($value[$relation['collection_one']] as $v) { + foreach ($value[$info->fieldName] as $v) { $data[] = $v[$relation['field_many']]; } return $data; diff --git a/src/core/Directus/Mail/Exception/MailNotSentException.php b/src/core/Directus/Mail/Exception/MailNotSentException.php new file mode 100644 index 0000000000..512e7bb145 --- /dev/null +++ b/src/core/Directus/Mail/Exception/MailNotSentException.php @@ -0,0 +1,16 @@ +canReadOnce($collection)) { - throw new Exception\ForbiddenCollectionReadException( - $collection - ); + // If a collection can't be accessed by the public group and user not logged in, ACL will return the unauthorized exception otherwise it will return forbidden error. + if($this->isPublic()){ + throw new UnauthorizedException('Unauthorized request'); + }else{ + throw new Exception\ForbiddenCollectionReadException( + $collection + ); + } } } diff --git a/src/core/Directus/Services/AbstractService.php b/src/core/Directus/Services/AbstractService.php index 705533a6db..213d3f1d7b 100644 --- a/src/core/Directus/Services/AbstractService.php +++ b/src/core/Directus/Services/AbstractService.php @@ -219,6 +219,10 @@ protected function createConstraintFor($collectionName, array $fields = [], $ski if ($field->hasAutoIncrement()) { continue; } + + if($field->isSystemDateTimeType() || $field->isSystemUserType()){ + continue; + } if ($field->getName() == "password" && isset($params['select_existing_or_update'])) { continue; diff --git a/src/core/Directus/Services/AuthService.php b/src/core/Directus/Services/AuthService.php index 654ee6f8b0..7efd2ee14f 100644 --- a/src/core/Directus/Services/AuthService.php +++ b/src/core/Directus/Services/AuthService.php @@ -2,6 +2,9 @@ namespace Directus\Services; +use function Directus\get_directus_path; +use function Directus\get_api_project_from_request; +use function Directus\get_url; use Directus\Authentication\Exception\ExpiredRequestTokenException; use Directus\Authentication\Exception\InvalidRequestTokenException; use Directus\Authentication\Exception\InvalidTokenException; @@ -10,17 +13,21 @@ use Directus\Authentication\Exception\InvalidResetPasswordTokenException; use Directus\Authentication\Exception\UserNotFoundException; use Directus\Authentication\Exception\UserWithEmailNotFoundException; +use Directus\Authentication\Exception\TFAEnforcedException; use Directus\Authentication\Sso\OneSocialProvider; use Directus\Authentication\Provider; use Directus\Authentication\Sso\Social; use Directus\Authentication\Sso\TwoSocialProvider; use Directus\Authentication\User\UserInterface; +use Directus\Database\Schema\SchemaManager; use Directus\Database\TableGateway\DirectusActivityTableGateway; +use Directus\Database\TableGateway\DirectusUserSessionsTableGateway; use Directus\Exception\UnauthorizedException; use Directus\Exception\UnprocessableEntityException; use Directus\Util\ArrayUtils; use Directus\Util\JWTUtils; use Directus\Util\StringUtils; +use Zend\Db\Sql\Update; class AuthService extends AbstractService { @@ -37,7 +44,7 @@ class AuthService extends AbstractService * * @throws UnauthorizedException */ - public function loginWithCredentials($email, $password, $otp=null) + public function loginWithCredentials($email, $password, $otp=null, $mode = null) { $this->validateCredentials($email, $password, $otp); @@ -63,17 +70,56 @@ public function loginWithCredentials($email, $password, $otp=null) $usersService = new UsersService($this->container); $tfa_enforced = $usersService->has2FAEnforced($user->getId()); - if ($tfa_enforced && $user->get2FASecret() == null) { - $token = $this->generateAuthToken($user, true); - } else { - $token = $this->generateAuthToken($user); + switch($mode){ + case DirectusUserSessionsTableGateway::TOKEN_COOKIE : + $user = $this->findOrCreateStaticToken($user); + $responseData['user'] = $user; + break; + case DirectusUserSessionsTableGateway::TOKEN_JWT : + default : + $token = $this->generateAuthToken($user); + $user = $user->toArray(); + $responseData = [ + 'token' => $token, + 'user' => $user + ]; + } + $responseObject['data'] = $responseData; + + if(!is_null($user)){ + $needs2FA = $tfa_enforced && $user['2fa_secret'] == null; + if($needs2FA){ + $responseObject['error'] = [ + 'code' => TFAEnforcedException::ERROR_CODE, + 'message' => TFAEnforcedException::ERROR_MESSAGE + ]; + } + } + return $responseObject; + } - return [ - 'data' => [ - 'token' => $token - ] - ]; + /** + * @param array $user + * + * @return array + * + */ + public function findOrCreateStaticToken(&$user) + { + $user = $user->toArray(); + if(empty($user['token'])){ + $token = StringUtils::randomString(24,false); + $userTable = $this->createTableGateway(SchemaManager::COLLECTION_USERS, false); + $Update = new Update(SchemaManager::COLLECTION_USERS); + $Update->set(['token' => $token]); + $Update->where([ + 'id' => $user['id'] + ]); + $userTable->updateWith($Update); + $user['token'] = $token; + } + return $user; } /** @@ -172,7 +218,7 @@ public function getSsoInfo($name) ); } - public function handleAuthenticationRequestCallback($name, $generateRequestToken = false) + public function handleAuthenticationRequestCallback($name, $generateRequestToken = false, $mode= null) { /** @var Social $socialAuth */ $socialAuth = $this->container->get('external_auth'); @@ -182,16 +228,23 @@ public function handleAuthenticationRequestCallback($name, $generateRequestToken $serviceUser = $service->handle(); $user = $this->authenticateWithEmail($serviceUser->getEmail()); - if ($generateRequestToken) { - $token = $this->generateRequestToken($user); - } else { - $token = $this->generateAuthToken($user); + + switch($mode){ + case DirectusUserSessionsTableGateway::TOKEN_COOKIE : + $user = $this->findOrCreateStaticToken($user); + $responseData['user'] = $user; + break; + case DirectusUserSessionsTableGateway::TOKEN_JWT : + default : + $token = $generateRequestToken ? $this->generateRequestToken($user) : $this->generateAuthToken($user); + $responseData = [ + 'token' => $token, + 'user' => $user->toArray() + ]; } return [ - 'data' => [ - 'token' => $token - ] + 'data' => $responseData ]; } @@ -303,16 +356,14 @@ public function authenticateWithSsoRequestToken($token) * * @param UserInterface $user * - * @param bool $needs2FA Whether the user needs 2FA - * * @return string */ - public function generateAuthToken(UserInterface $user, bool $needs2FA = false) + public function generateAuthToken(UserInterface $user) { /** @var Provider $auth */ $auth = $this->container->get('auth'); - return $auth->generateAuthToken($user, $needs2FA); + return $auth->generateAuthToken($user); } /** @@ -336,6 +387,7 @@ public function generateRequestToken(UserInterface $user) * @param $email */ public function sendResetPasswordToken($email) + { $this->validate(['email' => $email], ['email' => 'required|email']); @@ -345,10 +397,14 @@ public function sendResetPasswordToken($email) $resetToken = $auth->generateResetPasswordToken($user); - \Directus\send_forgot_password_email($user->toArray(), $resetToken); + // Sending the project key in the query param makes sure the app will use the correct project + // to send the new password to + $resetUrl = get_url() . 'admin/#/reset-password?token=' . $resetToken . '&project=' . get_api_project_from_request(); + + \Directus\send_forgot_password_email($user->toArray(), $resetUrl); } - public function resetPasswordWithToken($token) + public function resetPasswordWithToken($token, $newPassword) { if (!JWTUtils::isJWT($token)) { throw new InvalidResetPasswordTokenException($token); @@ -379,12 +435,9 @@ public function resetPasswordWithToken($token) throw new InvalidResetPasswordTokenException($token); } - $newPassword = StringUtils::randomString(16); $userProvider->update($user, [ 'password' => $auth->hashPassword($newPassword) ]); - - \Directus\send_reset_password_email($user->toArray(), $newPassword); } public function refreshToken($token) diff --git a/src/core/Directus/Services/ItemsService.php b/src/core/Directus/Services/ItemsService.php index fc0c0c61c0..33e0af03e6 100644 --- a/src/core/Directus/Services/ItemsService.php +++ b/src/core/Directus/Services/ItemsService.php @@ -169,14 +169,13 @@ public function validateParentCollectionFields($collection, $payload, $params, $ $collectionFields = $payload; foreach($tableColumns as $key => $column){ - if(!empty($recordData)){ + if(!empty($recordData) && array_key_exists($column->getName(), $recordData)){ $columnName = $column->getName(); - - $collectionFields[$columnName] = array_key_exists($column->getName(), $collectionFields) ? $collectionFields[$column->getName()]: (DataTypes::isJson($column->getType()) ? (array) $recordData[$columnName] : $recordData[$columnName]); + + $collectionFields[$columnName] = array_key_exists($columnName, $collectionFields) ? $collectionFields[$columnName]: (DataTypes::isJson($column->getType()) ? (array) $recordData[$columnName] : $recordData[$columnName]); } } - $this->validatePayload($collection, null, $collectionFields, $params); } @@ -251,7 +250,7 @@ public function validateAliasCollection($payload, $params, $aliasColumnDetails, if(!isset($individual['$delete'])){ foreach($relationalCollectionColumns as $key => $column){ - if(!$column->isAlias() && !$column->hasPrimaryKey() && !empty($individual[$relationalCollectionPrimaryKey])){ + if(!$column->hasPrimaryKey() && !empty($individual[$relationalCollectionPrimaryKey])){ $columnName = $column->getName(); $relationalCollectionData = $this->findByIds( $relationalCollectionName, diff --git a/src/core/Directus/Services/ProjectService.php b/src/core/Directus/Services/ProjectService.php index 2097804a57..cca3c8d1f6 100644 --- a/src/core/Directus/Services/ProjectService.php +++ b/src/core/Directus/Services/ProjectService.php @@ -8,6 +8,7 @@ use Directus\Exception\InvalidDatabaseConnectionException; use Directus\Exception\InvalidPathException; use Directus\Exception\NotFoundException; +use Directus\Exception\UnauthorizedException; use Directus\Exception\ProjectAlreadyExistException; use Directus\Exception\UnprocessableEntityException; use Directus\Util\ArrayUtils; @@ -21,11 +22,12 @@ public function create(array $data) throw new ForbiddenException('Creating new instance is locked'); } - $this->validate($data, [ - 'project' => 'string|regex:/^[0-9a-z_-]+$/i', - + $this->validate($data,[ + 'project' => 'required|string|regex:/^[0-9a-z_-]+$/i', + 'private' => 'bool', 'force' => 'bool', 'existing' => 'bool', + 'super_admin_token' => 'required', 'db_host' => 'string', 'db_port' => 'numeric', @@ -53,7 +55,23 @@ public function create(array $data) 'user_email' => 'required|email', 'user_password' => 'required|string', 'user_token' => 'string' - ]); + ]); + + // If the first installtion is executing then add the api.json file to store the password. + // For every installation after the first one, user must pass that same password to create the next project. + + $scannedDirectory = \Directus\scan_config_folder(); + + $superadminFilePath = \Directus\get_app_base_path().'/config/__api.json'; + if(empty($scannedDirectory)){ + $configStub = InstallerUtils::createJsonFileContent($data); + file_put_contents($superadminFilePath, $configStub); + }else{ + $superadminFileData = json_decode(file_get_contents($superadminFilePath), true); + if ($data['super_admin_token'] !== $superadminFileData['super_admin_token']) { + throw new UnauthorizedException('Permission denied: Superadmin Only'); + } + } $basePath = $this->container->get('path_base'); $force = ArrayUtils::pull($data, 'force', false); @@ -65,14 +83,10 @@ public function create(array $data) } $projectName = ArrayUtils::pull($data, 'project'); - if (empty($projectName)) { - $projectName = '_'; - } - $data['project'] = $projectName; try { - InstallerUtils::ensureCanCreateConfig($basePath, $data, $force); + InstallerUtils::ensureCanCreateConfig($basePath, $data, $force); } catch (InvalidConfigPathException $e) { throw new ProjectAlreadyExistException($projectName); } @@ -96,6 +110,7 @@ public function create(array $data) } } + /** * Deletes a project with the given name * diff --git a/src/core/Directus/Services/ServerService.php b/src/core/Directus/Services/ServerService.php index 558fb89bc9..5b61d3b52d 100644 --- a/src/core/Directus/Services/ServerService.php +++ b/src/core/Directus/Services/ServerService.php @@ -5,6 +5,7 @@ use Directus\Application\Application; use Directus\Exception\UnauthorizedException; use function Directus\get_project_info; +use Directus\Services\UsersService; class ServerService extends AbstractService { @@ -23,17 +24,22 @@ class ServerService extends AbstractService */ public function findAllInfo($global = true, $configuration = null) { + $acl = $this->container->get('acl'); + $usersService = new UsersService($this->container); + $tfa_enforced = $usersService->has2FAEnforced($acl->getUserId()); + if ($configuration === null) { $configuration = self::INFO_SETTINGS_RUNTIME; } $data = [ 'api' => [ - 'version' => Application::DIRECTUS_VERSION + 'version' => Application::DIRECTUS_VERSION, + 'requires2FA' => $tfa_enforced ], 'server' => [ 'max_upload_size' => \Directus\get_max_upload_size($configuration === self::INFO_SETTINGS_CORE), - ] + ], ]; if ($global !== true) { @@ -54,6 +60,28 @@ public function findAllInfo($global = true, $configuration = null) ]; } + /** + * Return Project public data + * + * @return array + */ + public function validateServerInfo($data) + { + $scannedDirectory = \Directus\scan_config_folder(); + + $superadminFilePath = \Directus\get_app_base_path().'/config/__api.json'; + if(!empty($scannedDirectory)){ + $this->validate($data, [ + 'super_admin_token' => 'required' + ]); + $superadminFileData = json_decode(file_get_contents($superadminFilePath), true); + if ($data['super_admin_token'] !== $superadminFileData['super_admin_token']) { + throw new UnauthorizedException('Permission denied: Superadmin Only'); + } + } + + } + /** * Return Project public data * diff --git a/src/core/Directus/Services/UserSessionService.php b/src/core/Directus/Services/UserSessionService.php new file mode 100644 index 0000000000..450c3f46b1 --- /dev/null +++ b/src/core/Directus/Services/UserSessionService.php @@ -0,0 +1,89 @@ +collection = SchemaManager::COLLECTION_USER_SESSIONS; + } + + /** + * @param array $data + * + * @return array + */ + public function create(array $data) + { + $userSessiontableGateway = $this->createTableGateway($this->collection, false); + return $userSessiontableGateway->recordSession($data); + } + + /** + * @param $id + * @param array $data + * + * @return array + */ + public function update($id,array $data) + { + $userSessiontableGateway = $this->createTableGateway($this->collection, false); + return $userSessiontableGateway->updateSession($id,$data); + } + + /** + * @param array $conditions + * + * @return string + * + */ + public function findAll($conditions) + { + return $this->getItemsAndSetResponseCacheTags($this->createTableGateway($this->collection,false), $conditions); + } + + /** + * @param array $conditions + * + * @return string + * + */ + public function find($conditions) + { + $userSessiontableGateway = $this->createTableGateway($this->collection, false); + $response = $userSessiontableGateway->fetchSession($conditions); + return $response ? $response->toArray() : $response; + } + + /** + * @param $conditions + * + */ + public function destroy($conditions) + { + $userSessionTable = $this->createTableGateway(SchemaManager::COLLECTION_USER_SESSIONS, false); + $userSessionTable->delete($conditions); + return true; + } +} diff --git a/src/core/Directus/Services/UsersService.php b/src/core/Directus/Services/UsersService.php index c44caf5335..dee08a78f1 100644 --- a/src/core/Directus/Services/UsersService.php +++ b/src/core/Directus/Services/UsersService.php @@ -429,8 +429,8 @@ protected function enforceLastAdmin($id) public function activate2FA($id, $tfa_secret, $otp) { $this->validate( - ['tfa_secret' => $tfa_secret, 'otp' => $otp], - ['tfa_secret' => 'required|string', 'otp' => 'required|string'] + ['2fa_secret' => $tfa_secret, 'otp' => $otp], + ['2fa_secret' => 'required|string', 'otp' => 'required|string'] ); $ga = new Google2FA(); diff --git a/src/core/Directus/Services/UtilsService.php b/src/core/Directus/Services/UtilsService.php index 1c052b74b8..8cb3bc9b09 100644 --- a/src/core/Directus/Services/UtilsService.php +++ b/src/core/Directus/Services/UtilsService.php @@ -69,6 +69,10 @@ public function generate2FASecret() { $ga = new Google2FA(); $tfa_secret = $ga->generateSecretKey(); - return ['2fa_secret' => $tfa_secret]; + return [ + 'data' => [ + '2fa_secret' => $tfa_secret + ] + ]; } } diff --git a/src/core/Directus/Services/WebhookService.php b/src/core/Directus/Services/WebhookService.php new file mode 100644 index 0000000000..44e25f0f04 --- /dev/null +++ b/src/core/Directus/Services/WebhookService.php @@ -0,0 +1,256 @@ +collection = SchemaManager::COLLECTION_WEBHOOKS; + $this->itemsService = new ItemsService($this->container); + } + + /** + * @param array $params + * + * @return array + */ + public function findAll(array $params = [],$acl = false) + { + return $this->getItemsAndSetResponseCacheTags($this->getTableGateway($acl), $params); + } + + /** + * @param array $data + * + * @return array + */ + public function create(array $data, array $params = []) + { + return $this->itemsService->createItem($this->collection, $data, $params); + } + + /** + * @param $id + * @param array $data + * + * @return array + */ + public function update($id, array $payload, array $params = []) + { + $this->enforceUpdatePermissions($this->collection, $payload, $params); + $this->validatePayload($this->collection, array_keys($payload), $payload, $params); + + $this->getTableGateway()->updateRecord($id, $payload, $this->getCRUDParams($params)); + + try { + $item = $this->find( + $id, + ArrayUtils::omit($params, $this->itemsService::SINGLE_ITEM_PARAMS_BLACKLIST) + ); + } catch (\Exception $e) { + $item = null; + } + + return $item; + } + + public function find($id, array $params = []) + { + return $this->itemsService->find( + $this->collection, + $id, + $params + ); + } + + public function findByIds($id, array $params = []) + { + return $this->itemsService->findByIds( + $this->collection, + $id, + $params + ); + } + + public function findOne(array $params = []) + { + return $this->itemsService->findOne( + $this->collection, + $params + ); + } + + public function delete($id, array $params = []) + { + $this->enforcePermissions($this->collection, [], $params); + $tableGateway = $this->getTableGateway(); + + // hotfix: enforce delete permission before checking for the item existence + // this avoids an indirect reveal of an item the user is not allowed to see + $delete = new Delete($this->collection); + $delete->where([ + 'id' => $id + ]); + $tableGateway->enforceDeletePermission($delete); + + $tableGateway->deleteRecord($id, $this->getCRUDParams($params)); + + return true; + } + /** + * Gets the webhook table gateway + * + * @return RelationalTableGateway + */ + public function getTableGateway($acl=true) + { + if (!$this->tableGateway) { + $this->tableGateway = $this->createTableGateway($this->collection,$acl); + } + + return $this->tableGateway; + } + + /** + * @param $collection + * @param array $items + * @param array $params + * + * @return array + * + * @throws InvalidRequestException + */ + public function batchCreate(array $items, array $params = []) + { + if (!isset($items[0]) || !is_array($items[0])) { + throw new InvalidRequestException('batch create expect an array of items'); + } + + foreach ($items as $data) { + $this->enforceCreatePermissions($this->collection, $data, $params); + $this->validatePayload($this->collection, null, $data, $params); + } + + $allItems = []; + foreach ($items as $data) { + $item = $this->create($data, $params); + if (!is_null($item)) { + $allItems[] = $item['data']; + } + } + + if (!empty($allItems)) { + $allItems = ['data' => $allItems]; + } + + return $allItems; + } + + /** + * @param $collection + * @param array $items + * @param array $params + * + * @return array + * + * @throws InvalidRequestException + */ + public function batchUpdate(array $items, array $params = []) + { + if (!isset($items[0]) || !is_array($items[0])) { + throw new InvalidRequestException('batch update expect an array of items'); + } + + foreach ($items as $data) { + $this->enforceCreatePermissions($this->collection, $data, $params); + $this->validatePayload($this->collection, array_keys($data), $data, $params); + $this->validatePayloadHasPrimaryKey($this->collection, $data); + } + + $collectionObject = $this->getSchemaManager()->getCollection($this->collection); + $allItems = []; + foreach ($items as $data) { + $id = $data[$collectionObject->getPrimaryKeyName()]; + $item = $this->update($id, $data, $params); + + if (!is_null($item)) { + $allItems[] = $item['data']; + } + } + + if (!empty($allItems)) { + $allItems = ['data' => $allItems]; + } + + return $allItems; + } + + /** + * @param $collection + * @param array $ids + * @param array $payload + * @param array $params + * + * @return array + */ + public function batchUpdateWithIds(array $ids, array $payload, array $params = []) + { + $this->enforceUpdatePermissions($this->collection, $payload, $params); + $this->validatePayload($this->collection, array_keys($payload), $payload, $params); + + $allItems = []; + foreach ($ids as $id) { + $item = $this->update($id, $payload, $params); + if (!empty($item)) { + $allItems[] = $item['data']; + } + } + + if (!empty($allItems)) { + $allItems = ['data' => $allItems]; + } + + return $allItems; + } + + /** + * @param $collection + * @param array $ids + * @param array $params + * + * @throws ForbiddenException + */ + public function batchDeleteWithIds(array $ids, array $params = []) + { + foreach ($ids as $id) { + $this->delete($id, $params); + } + } + +} \ No newline at end of file diff --git a/src/core/Directus/Util/DateTimeUtils.php b/src/core/Directus/Util/DateTimeUtils.php index 08caf2fb39..522547362f 100644 --- a/src/core/Directus/Util/DateTimeUtils.php +++ b/src/core/Directus/Util/DateTimeUtils.php @@ -109,8 +109,14 @@ public static function nowInUTC() public static function nowInTimezone() { - $config = get_project_config(); - return static::now($config->get('app.timezone')); + $projectName = \Directus\get_api_project_from_request(); + if(!is_null($projectName)){ + $config = get_project_config($projectName); + return static::now($config->get('app.timezone')); + } else { + // If there's no project config (f.e. when creating projects), default to UTC + return static::now('UTC'); + } } /** diff --git a/src/core/Directus/Util/Installation/InstallerUtils.php b/src/core/Directus/Util/Installation/InstallerUtils.php index 66f2f98a59..49ff19f49f 100644 --- a/src/core/Directus/Util/Installation/InstallerUtils.php +++ b/src/core/Directus/Util/Installation/InstallerUtils.php @@ -18,16 +18,55 @@ use Directus\Permissions\Acl; use Directus\Util\ArrayUtils; use Directus\Util\StringUtils; +use Directus\Util\DateTimeUtils; use Phinx\Config\Config; use Phinx\Migration\Manager; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\NullOutput; use Zend\Db\Sql\Ddl\DropTable; use Zend\Db\Sql\Sql; +use Zend\Db\Sql\Select; use Zend\Db\TableGateway\TableGateway; class InstallerUtils { + const MIGRATION_CONFIGURATION_PATH = '/migrations/migrations.php'; + /** + * Add the upgraded migrations into directus_migration on a fresh installation of project. + * Upgraded migrations may contain the same queries which is in db [Original] migrations. + * So in every fresh installtion we have boycott the upgrades. + * This function will add the upgrades migrations into directus_migrations table; so the current upgraded migrations cant be executed. + * + * @return boolean + */ + public static function addUpgradeMigrations() + { + $dbConnection = Application::getInstance()->fromContainer('database'); + $migrationsTableGateway = new TableGateway(SchemaManager::COLLECTION_MIGRATIONS, $dbConnection); + + $select = new Select($migrationsTableGateway->table); + $select->columns(['version']); + $result = $migrationsTableGateway->selectWith($select)->toArray(); + $alreadyStoredMigrations = array_column($result, 'version'); + + $ignoreableFiles = ['..', '.']; + $scannedDirectory = array_values(array_diff(scandir(\Directus\get_app_base_path().'/migrations/upgrades/schemas'), $ignoreableFiles)); + foreach($scannedDirectory as $fileName){ + $data = []; + $fileNameObject = explode("_",$fileName,2); + $migrationName = explode(".",str_replace(' ', '',ucwords(str_replace('_', ' ', $fileNameObject[1]))),2); + $data = [ + 'version' => $fileNameObject[0], + 'migration_name' => $migrationName[0], + 'start_time' => DateTimeUtils::nowInTimezone()->toString(), + 'end_time' => DateTimeUtils::nowInTimezone()->toString() + ]; + + if(!in_array($data['version'],$alreadyStoredMigrations) && !is_null($data['version']) && !is_null($data['migration_name'])){ + $migrationsTableGateway->insert($data); + } + } + } /** * Check if environment is using files or environment variables * @@ -86,6 +125,22 @@ public static function createConfigFileContent($data) return static::replacePlaceholderValues($configStub, ArrayUtils::pick($data, 'project')); } + + /** + * Creates a api json file to store the superadmin password + * + * @param string $name + * + * @throws NotFoundException + * @throws UnprocessableEntityException + */ + public static function createJsonFileContent($data) + { + $configStub = file_get_contents(__DIR__ . '/stubs/api.stub'); + + return static::replacePlaceholderValues($configStub, $data); + } + /** * Replace placeholder wrapped by {{ }} with $data array * @@ -252,12 +307,10 @@ public static function addDefaultSettings($basePath, array $data, $projectName = { $basePath = rtrim($basePath, '/'); static::ensureConfigFileExists($basePath, $projectName); - $app = static::createApp($basePath, $projectName); $db = $app->getContainer()->get('database'); $defaultSettings = static::getDefaultSettings($data); - $tableGateway = new TableGateway('directus_settings', $db); foreach ($defaultSettings as $setting) { $tableGateway->insert($setting); @@ -397,15 +450,10 @@ public static function installSchemaFromSQL($sql) * * @return string */ - public static function getConfigName($projectName) + public static function getConfigName($projectName,$private=false) { - $name = 'api'; - - if ($projectName && $projectName !== '_') { - $name = sprintf('api.%s', $projectName); - } - - return $name; + $text = $private ? "private.%s" : "%s"; + return sprintf($text, $projectName); } /** @@ -417,7 +465,8 @@ public static function getConfigName($projectName) */ public static function ensureCanCreateConfig($path, array $data, $force = false) { - $configPath = static::createConfigPathFromData($path, $data); + $input = ArrayUtils::omit($data, ['private']); + $configPath = static::createConfigPathFromData($path, $input); static::ensureDirectoryIsWritable($path); if ($force !== true) { @@ -449,9 +498,15 @@ public static function deleteConfigFile($path, $projectName = null) * * @return string */ - public static function createConfigPath($path, $projectName = null) + public static function createConfigPath($path, $projectName, $private=null) { - $configName = static::getConfigName($projectName); + if(!is_null($private)){ + $configName = static::getConfigName($projectName,$private); + }else{ + $publicConfig = static::getConfigName($projectName); + $privateConfig = static::getConfigName($projectName,true); + $configName = file_exists($path . '/config/' . $privateConfig . '.php') ? $privateConfig : $publicConfig; + } return $path . '/config/' . $configName . '.php'; } @@ -467,14 +522,6 @@ public static function getDefaultPermissions() 'comment' => Acl::COMMENT_LEVEL_UPDATE, 'explain' => Acl::EXPLAIN_LEVEL_NONE, ], - SchemaManager::COLLECTION_ACTIVITY_SEEN => [ - 'create' => Acl::LEVEL_FULL, - 'read' => Acl::LEVEL_MINE, - 'update' => Acl::LEVEL_MINE, - 'delete' => Acl::LEVEL_MINE, - 'comment' => Acl::COMMENT_LEVEL_NONE, - 'explain' => Acl::EXPLAIN_LEVEL_NONE, - ], SchemaManager::COLLECTION_COLLECTION_PRESETS => [ 'create' => Acl::LEVEL_FULL, 'read' => Acl::LEVEL_FULL, @@ -618,7 +665,7 @@ public static function getDefaultPermissions() * * @throws \Exception */ - public static function ensureConfigFileExists($basePath, $projectName = null) + public static function ensureConfigFileExists($basePath, $projectName) { if (!self::isUsingFiles()) { return; @@ -645,7 +692,7 @@ public static function ensureConfigFileExists($basePath, $projectName = null) */ private static function createConfigPathFromData($path, array $data) { - return static::createConfigPath($path, ArrayUtils::get($data, 'project')); + return static::createConfigPath($path, ArrayUtils::get($data, 'project'), ArrayUtils::get($data, 'private')); } /** @@ -680,7 +727,6 @@ private static function getMigrationConfig($basePath, $projectName = null, $migr if ($migrationName === null) { $migrationName = 'db'; } - $configPath = static::createConfigPath($basePath, $projectName); $migrationPath = $basePath . '/migrations/' . $migrationName; @@ -697,7 +743,7 @@ private static function getMigrationConfig($basePath, $projectName = null, $migr ArrayUtils::rename($apiConfig, 'socket', 'unix_socket'); $apiConfig['charset'] = ArrayUtils::get($apiConfig, 'database.charset', 'utf8mb4'); - $configArray = require $basePath . '/config/migrations.php'; + $configArray = require $basePath.self::MIGRATION_CONFIGURATION_PATH ; $configArray['paths']['migrations'] = $migrationPath . '/schemas'; $configArray['paths']['seeds'] = $migrationPath . '/seeds'; $configArray['environments']['development'] = $apiConfig; @@ -733,7 +779,7 @@ private static function getDefaultSettings($data) */ private static function ensureMigrationFileExists($basePath) { - $migrationConfigPath = $basePath . '/config/migrations.php'; + $migrationConfigPath = $basePath . self::MIGRATION_CONFIGURATION_PATH; if (!file_exists($migrationConfigPath)) { throw new InvalidPathException( @@ -799,7 +845,7 @@ private static function ensureSystemTablesDoesNotExistsFromData(array $data) { static::getDirectusTablesFromData($data, function (Connection $db, $name) { throw new Exception( - sprintf('Directus seems to has been installed in the "%s" database.', $db->getCurrentSchema()) + sprintf('The "%s" database already contains Directus system tables.', $db->getCurrentSchema()) ); }); } @@ -882,7 +928,7 @@ private static function createSchemaManagerFromData(array $data, Connection $db */ private static function dropTables($basePath, $projectName) { - $app = static::createApp($basePath, $config); + $app = static::createApp($basePath, $projectName); /** @var Connection $db */ $db = $app->getContainer()->get('database'); /** @var SchemaManager $schemaManager */ @@ -956,7 +1002,7 @@ private static function createConfigData(array $data) 'headers' => [], 'exposed_headers' => [], 'max_age' => 600, - 'credentials' => false, + 'credentials' => true, ], 'rate_limit' => [ 'enabled' => false, diff --git a/src/core/Directus/Util/Installation/stubs/api.stub b/src/core/Directus/Util/Installation/stubs/api.stub new file mode 100644 index 0000000000..2df8125846 --- /dev/null +++ b/src/core/Directus/Util/Installation/stubs/api.stub @@ -0,0 +1,3 @@ +{ + "super_admin_token": "{{super_admin_token}}" +} \ No newline at end of file diff --git a/src/endpoints/Auth.php b/src/endpoints/Auth.php index f0f02da14f..91ac1c3d21 100644 --- a/src/endpoints/Auth.php +++ b/src/endpoints/Auth.php @@ -7,10 +7,22 @@ use Directus\Application\Http\Response; use Directus\Application\Route; use function Directus\array_get; +use function Directus\get_directus_setting; +use function Directus\get_directus_path; +use function Directus\get_project_session_cookie_name; +use function Directus\get_request_authorization_token; +use function Directus\encrypt_static_token; +use function Directus\decrypt_static_token; use Directus\Authentication\Exception\UserWithEmailNotFoundException; +use Directus\Authentication\Exception\SsoNotAllowedException; use Directus\Authentication\Sso\Social; use Directus\Services\AuthService; +use Directus\Services\UsersService; +use Directus\Services\UserSessionService; use Directus\Util\ArrayUtils; +use Slim\Http\Cookies; +use Directus\Database\TableGateway\DirectusUserSessionsTableGateway; +use Directus\Mail\Exception\MailNotSentException; class Auth extends Route { @@ -20,8 +32,12 @@ class Auth extends Route public function __invoke(Application $app) { $app->post('/authenticate', [$this, 'authenticate']); + $app->get('/sessions', [$this, 'userSessions']); + $app->post('/logout', [$this, 'logout']); + $app->post('/logout/{user}', [$this, 'logoutFromAll']); + $app->post('/logout/{user}/{id}', [$this, 'logoutFromOne']); $app->post('/password/request', [$this, 'forgotPassword']); - $app->get('/password/reset/{token}', [$this, 'resetPassword']); + $app->post('/password/reset', [$this, 'resetPassword']); $app->post('/refresh', [$this, 'refresh']); $app->get('/sso', [$this, 'listSsoAuthServices']); $app->post('/sso/access_token', [$this, 'ssoAccessToken']); @@ -47,12 +63,161 @@ public function authenticate(Request $request, Response $response) $responseData = $authService->loginWithCredentials( $request->getParsedBodyParam('email'), $request->getParsedBodyParam('password'), - $request->getParsedBodyParam('otp') + $request->getParsedBodyParam('otp'), + $request->getParam('mode') ); + if(isset($responseData['data']) && isset($responseData['data']['user'])){ + switch($request->getParam('mode')){ + case DirectusUserSessionsTableGateway::TOKEN_COOKIE : + $response = $this->storeCookieSession($request,$response,$responseData['data']); + break; + case DirectusUserSessionsTableGateway::TOKEN_JWT : + default : + $this->storeJwtSession($responseData['data']); + } + unset($responseData['data']['user']); + } + $responseData['data'] = !empty($responseData['data']) ? $responseData['data'] : null; + return $this->responseWithData($request, $response, $responseData); + } + + /** + * Return the session history of given user + * + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function userSessions(Request $request, Response $response) + { + $responseData = []; + $authorizationTokenObject = get_request_authorization_token($request); + if(isset($authorizationTokenObject['type'])){ + $accessToken = $authorizationTokenObject['type'] == DirectusUserSessionsTableGateway::TOKEN_COOKIE ? decrypt_static_token($authorizationTokenObject['token']) : $authorizationTokenObject['token']; + $userSessionService = new UserSessionService($this->container); + $userSession = $userSessionService->find(['token' => $accessToken]); + if($userSession){ + $responseData = $userSessionService->findAll(['user' => $userSession['user']]); + } + } return $this->responseWithData($request, $response, $responseData); } + /** + * Generate cookie token and store it into user sessions table + * + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function storeCookieSession($request,$response,$data){ + $authorizationTokenObject = get_request_authorization_token($request); + $expirationMinutes = get_directus_setting('auto_sign_out'); + $expiry = new \DateTimeImmutable('now + '.$expirationMinutes.'minutes'); + $userSessionService = new UserSessionService($this->container); + if(!empty($authorizationTokenObject['token'])){ + $accessToken = decrypt_static_token($authorizationTokenObject['token']); + $userSessionObject = $userSessionService->find(['token' => $accessToken]); + $sessionToken = $userSessionObject['token']; + }else{ + $userSession = $userSessionService->create([ + 'user' => $data['user']['id'], + 'token' => $data['user']['token'], + 'token_type' => DirectusUserSessionsTableGateway::TOKEN_COOKIE, + 'token_expired_at' => $expiry->format('Y-m-d H:i:s') + ]); + $sessionToken = $data['user']['token']."-".$userSession; + $userSessionService->update($userSession,['token' => $sessionToken]); + } + + $cookie = new Cookies(); + $cookie->set( + get_project_session_cookie_name($request), + [ + 'value' => encrypt_static_token($sessionToken), + 'expires' => $expiry->format(\DateTime::COOKIE), + 'path'=>'/', + 'httponly' => true + ] + ); + + return $response->withAddedHeader('Set-Cookie',$cookie->toHeaders()); + } + + /** + * Generate jwt token and store login entry into user sessions table + * + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function storeJwtSession($data){ + $expirationMinutes = get_directus_setting('auto_sign_out'); + $expiry = new \DateTimeImmutable('now + '.$expirationMinutes.'minutes'); + $userSessionService = new UserSessionService($this->container); + $userSessionService->create([ + 'user' => $data['user']['id'], + 'token' => $data['token'], + 'token_type' => DirectusUserSessionsTableGateway::TOKEN_JWT, + 'token_expired_at' => $expiry->format('Y-m-d H:i:s') + ]); + } + + /** + * Logout user's current session + * + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function logout(Request $request, Response $response) + { + $authorizationTokenObject = get_request_authorization_token($request); + $accessToken = $authorizationTokenObject['type'] == DirectusUserSessionsTableGateway::TOKEN_COOKIE ? decrypt_static_token($authorizationTokenObject['token']) : $authorizationTokenObject['token']; + $userSessionService = new UserSessionService($this->container); + $userSessionService->destroy(['token' => $accessToken]); + return $this->responseWithData($request, $response, []); + } + + /** + * Logout from user's all session + * + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function logoutFromAll(Request $request, Response $response) + { + $userSessionService = new UserSessionService($this->container); + $responseData = $userSessionService->destroy(['user' => $request->getAttribute('user')]); + return $this->responseWithData($request, $response, $responseData); + } + + /** + * Logout from user's particular session + * + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function logoutFromOne(Request $request, Response $response) + { + $userSessionService = new UserSessionService($this->container); + $responseData = $userSessionService->destroy([ + 'id' => $request->getAttribute('id'), + 'user' => $request->getAttribute('user') + ]); + return $this->responseWithData($request, $response, $responseData); + } + + /** * Sends a user a token to reset its password * @@ -73,6 +238,7 @@ public function forgotPassword(Request $request, Response $response) ); } catch (\Exception $e) { $this->container->get('logger')->error($e); + throw new MailNotSentException(); } return $this->responseWithData($request, $response, []); @@ -90,7 +256,8 @@ public function resetPassword(Request $request, Response $response) $authService = $this->container->get('services')->get('auth'); $authService->resetPasswordWithToken( - $request->getAttribute('token') + $request->getParsedBodyParam('token'), + $request->getParsedBodyParam('password') ); return $this->responseWithData($request, $response, []); @@ -126,6 +293,7 @@ public function listSsoAuthServices(Request $request, Response $response) { /** @var AuthService $authService */ $authService = $this->container->get('services')->get('auth'); + /** @var Social $externalAuth */ $externalAuth = $this->container->get('external_auth'); @@ -158,13 +326,14 @@ public function ssoService(Request $request, Response $response) $responseData = $authService->getAuthenticationRequestInfo( $request->getAttribute('service') ); - + $session->set('mode', $request->getParam('mode')); + $session->set('redirect_url', $request->getParam('redirect_url')); if (\Directus\cors_is_origin_allowed($allowedOrigins, $origin)) { if (is_array($origin)) { $origin = array_shift($origin); } - $session->set('sso_origin_url', $origin); + $response = $response->withRedirect(array_get($responseData, 'data.authorization_url')); } @@ -202,20 +371,36 @@ public function ssoServiceCallback(Request $request, Response $response) { /** @var AuthService $authService */ $authService = $this->container->get('services')->get('auth'); - $session = $this->container->get('session'); - // TODO: Implement a pull method - $redirectUrl = $session->get('sso_origin_url'); - $session->remove('sso_origin_url'); + $session = $this->container->get('session'); + $mode = $session->get('mode'); + $redirectUrl = $session->get('redirect_url') ? $session->get('redirect_url') : $session->get('sso_origin_url'); $responseData = []; $urlParams = []; + try { $responseData = $authService->handleAuthenticationRequestCallback( $request->getAttribute('service'), - !!$redirectUrl + true, + $mode ); - $urlParams['request_token'] = array_get($responseData, 'data.token'); + if(isset($responseData['data']) && isset($responseData['data']['user'])){ + $usersService = new UsersService($this->container); + $tfa_enforced = $usersService->has2FAEnforced($responseData['data']['user']['id']); + if($tfa_enforced || !empty($responseData['data']['user']['2fa_secret'])){ + throw new SsoNotAllowedException(); + } + + switch($mode){ + case DirectusUserSessionsTableGateway::TOKEN_COOKIE : + $response = $this->storeCookieSession($request,$response,$responseData['data']); + break; + default : + $this->storeJwtSession($responseData['data']); + $urlParams['request_token'] = array_get($responseData, 'data.token'); + } + } } catch (\Exception $e) { if (!$redirectUrl) { throw $e; @@ -229,18 +414,25 @@ public function ssoServiceCallback(Request $request, Response $response) $urlParams['error'] = true; } + if ($redirectUrl) { $redirectQueryString = parse_url($redirectUrl, PHP_URL_QUERY); $redirectUrlParts = explode('?', $redirectUrl); $redirectUrl = $redirectUrlParts[0]; - $redirectQueryParams = parse_str($redirectQueryString); + parse_str($redirectQueryString, $redirectQueryParams); if (is_array($redirectQueryParams)) { $urlParams = array_merge($redirectQueryParams, $urlParams); } - $response = $response->withRedirect($redirectUrl . '?' . http_build_query($urlParams)); + $urlToRedirect = !empty($urlParams) ? $redirectUrl . '?' . http_build_query($urlParams) : $redirectUrl; + $response = $response->withRedirect($urlToRedirect); + + }else{ + $response = $response->withRedirect($redirectUrl); } + $session->remove('mode'); + $session->remove('redirect_url'); return $this->responseWithData($request, $response, $responseData); } diff --git a/src/endpoints/Home.php b/src/endpoints/Home.php index 0dd0db4d61..2c22bca07a 100644 --- a/src/endpoints/Home.php +++ b/src/endpoints/Home.php @@ -5,15 +5,12 @@ use Directus\Application\Http\Request; use Directus\Application\Http\Response; use Directus\Application\Route; -use Directus\Services\ServerService; class Home extends Route { public function __invoke(Request $request, Response $response) { - $service = new ServerService($this->container); - $responseData = $service->findAllInfo(); - - return $this->responseWithData($request, $response, $responseData); + $response = $response->withRedirect('./admin/'); + return $this->responseWithData($request, $response, []); } } diff --git a/src/endpoints/ProjectHome.php b/src/endpoints/ProjectHome.php index e915ee8f20..e28ca79e32 100644 --- a/src/endpoints/ProjectHome.php +++ b/src/endpoints/ProjectHome.php @@ -14,14 +14,14 @@ public function __invoke(Request $request, Response $response) { /** @var Acl $acl */ $acl = $this->container->get('acl'); - + $service = new ServerService($this->container); if ($acl->getUserId()) { $responseData = $service->findAllInfo(false); } else { $responseData = [ 'data' => [ - 'api' => $service->getPublicInfo() + 'api' => array_merge(['requires2FA' => false],$service->getPublicInfo()) ] ]; } diff --git a/src/endpoints/ProjectsCreate.php b/src/endpoints/ProjectsCreate.php index 19af1d9233..0cd0866292 100644 --- a/src/endpoints/ProjectsCreate.php +++ b/src/endpoints/ProjectsCreate.php @@ -6,6 +6,7 @@ use Directus\Application\Http\Response; use Directus\Application\Route; use Directus\Services\ProjectService; +use Directus\Util\Installation\InstallerUtils; class ProjectsCreate extends Route { @@ -14,7 +15,7 @@ public function __invoke(Request $request, Response $response) $this->validateRequestPayload($request); $installService = new ProjectService($this->container); $installService->create($request->getParsedBody()); - + InstallerUtils::addUpgradeMigrations(); return $this->responseWithData($request, $response, []); } } diff --git a/src/endpoints/Server.php b/src/endpoints/Server.php index ca5791126b..c18eb8482d 100644 --- a/src/endpoints/Server.php +++ b/src/endpoints/Server.php @@ -4,6 +4,11 @@ use Directus\Application\Application; use Directus\Application\Route; +use Directus\Application\Http\Request; +use Directus\Application\Http\Response; +use Directus\Exception\NotInstalledException; +use Directus\Util\StringUtils; +use Directus\Services\ServerService; class Server extends Route { @@ -13,5 +18,75 @@ class Server extends Route public function __invoke(Application $app) { \Directus\create_ping_route($app); + $app->get('/projects', [$this, 'projects']); + $app->get('/info', [$this, 'getInfo']); } + + /** + * Return the projects + * + * @return Response + */ + public function projects(Request $request, Response $response) + { + $scannedDirectory = \Directus\scan_config_folder(); + + $projectNames = []; + if(empty($scannedDirectory)){ + throw new NotInstalledException('This Directus instance has not been configured. Install via the Directus App (eg: /admin) or read more about configuration at: https://docs.directus.io/getting-started/installation.html#configure'); + }else{ + foreach($scannedDirectory as $fileName){ + if(!StringUtils::startsWith($fileName, 'private.')){ + $fileObject = explode(".",$fileName); + $projectNames[] = $fileObject[0]; + } + } + } + + $responseData['data'] = $projectNames; + return $this->responseWithData($request, $response, $responseData); + } + + /** + * Return the current setup of server. + * + * @return Response + */ + public function getInfo(Request $request, Response $response) + { + $data = $request->getQueryParams(); + $service = new ServerService($this->container); + $service->validateServerInfo($data); + + $basePath = $this->container->get('path_base'); + $responseData['data'] = [ + 'directus' => Application::DIRECTUS_VERSION, + 'server' => [ + 'type' => $_SERVER['SERVER_SOFTWARE'], + 'rewrites' => function_exists('apache_get_modules') ? in_array('mod_rewrite', apache_get_modules()) : null, + 'os' => PHP_OS, + 'os_version' => php_uname('v'), + ], + 'php' => [ + 'version' => phpversion(), + 'max_upload_size' => \Directus\get_max_upload_size(ServerService::INFO_SETTINGS_RUNTIME === ServerService::INFO_SETTINGS_CORE), + 'extensions' => [ + 'pdo' => defined('PDO::ATTR_DRIVER_NAME'), + 'mysqli' => extension_loaded("mysqli"), + 'curl' => extension_loaded("curl"), + 'gd' => extension_loaded("gd"), + 'fileinfo' => extension_loaded("fileinfo"), + 'libapache2_mod_php' => extension_loaded("libapache2-mod-php"), + 'mbstring' => extension_loaded("mbstring"), + 'json' => extension_loaded("json"), + ], + ], + 'permissions' => [ + 'public' => substr(sprintf('%o', fileperms($basePath."/public")), -4), + 'logs' => substr(sprintf('%o', fileperms($basePath."/logs")), -4), + 'uploads' => substr(sprintf('%o', fileperms($basePath."/public/uploads")), -4), + ] + ]; + return $this->responseWithData($request, $response, $responseData); + } } diff --git a/src/endpoints/Settings.php b/src/endpoints/Settings.php index c7e6f0d56a..8447753010 100644 --- a/src/endpoints/Settings.php +++ b/src/endpoints/Settings.php @@ -8,6 +8,7 @@ use Directus\Application\Route; use Directus\Services\SettingsService; use Directus\Services\FilesServices; +use Directus\Util\ArrayUtils; use function Directus\regex_numeric_ids; class Settings extends Route @@ -74,38 +75,49 @@ public function all(Request $request, Response $response) * Get all the fields of settings table to check the interface * */ - - $fieldData = $service->findAllFields( - $request->getQueryParams() - ); + $fieldData = $service->findAllFields(ArrayUtils::omit($request->getQueryParams(), 'single')); + + // this will return the value based on interface type + $fieldTypeValueResolver = function ($type, $value) use ($service){ + switch ($type) { + case 'file': + try{ + $fileInstance = $service->findFile($value); + return $fileInstance['data'] ?? null; + }catch(\Exception $e){ + return null; + } + default: + return $value; + } + }; + + // find the field definition that matches the field + $fieldDefinitionTypeResolver = function ($key) use ($fieldData){ + $fieldDefinition = array_filter($fieldData['data'], function ($definition) use ($key){ + return $definition['field'] === $key; + }); + $fieldDefinition = array_shift($fieldDefinition); + return $fieldDefinition['type'] ?? null; + }; + + $valueResolver = function ($row) use ($fieldTypeValueResolver, $fieldDefinitionTypeResolver){ + $fieldDefinitionType = $fieldDefinitionTypeResolver($row['key']); + $row['value'] = $fieldTypeValueResolver($fieldDefinitionType, $row['value']); + return $row; + }; /** * Generate the response object based on interface/type * */ - foreach ($fieldData['data'] as $fieldDefinition) { - // find position of field in $response['data'] - $index = array_search($fieldDefinition['field'], array_column($responseData['data'], 'key')); - if (false !== $index) { - - switch ($fieldDefinition['type']) { - case 'file': - if (!empty($responseData['data'][$index]['value'])) { - try{ - $fileInstance = $service->findFile($responseData['data'][$index]['value']); - }catch(\Exception $e){ - $responseData['data'][$index]['value'] = null; - } + $isSingle = (int)ArrayUtils::get($request->getQueryParams(), 'single', 0); - if (!empty($fileInstance['data'])) { - $responseData['data'][$index]['value'] = $fileInstance['data']; - } - } - break; - default: - break; - } - } + if($isSingle){ + $responseData['data'] = $valueResolver($responseData['data']); + } + else{ + $responseData['data'] = array_map($valueResolver, $responseData['data']); } return $this->responseWithData($request, $response, $responseData); diff --git a/src/endpoints/Users.php b/src/endpoints/Users.php index fec442e805..b436ea8463 100644 --- a/src/endpoints/Users.php +++ b/src/endpoints/Users.php @@ -37,7 +37,7 @@ public function __invoke(Application $app) $app->patch('/{id}/tracking/page', [$this, 'trackPage']); // Enable 2FA - $app->post('/{id}/activate2FA', [$this, 'activate2FA']); + $app->post('/{id}/activate_2fa', [$this, 'activate2FA']); } /** @@ -135,7 +135,7 @@ public function update(Request $request, Response $response) if (strpos($id, ',') !== false) { return $this->batch($request, $response); } - + $responseData = $service->update( $id, $payload, @@ -286,7 +286,7 @@ public function activate2FA(Request $request, Response $response) $service = new UsersService($this->container); $responseData = $service->activate2FA( $request->getAttribute('id'), - $request->getParsedBodyParam('tfa_secret'), + $request->getParsedBodyParam('2fa_secret'), $request->getParsedBodyParam('otp') ); diff --git a/src/endpoints/Webhook.php b/src/endpoints/Webhook.php new file mode 100644 index 0000000000..150adba3b9 --- /dev/null +++ b/src/endpoints/Webhook.php @@ -0,0 +1,217 @@ +get('', [$this, 'all']); + $app->post('', [$this, 'create']); + $app->get('/{id}', [$this, 'read']); + $app->patch('/{id}', [$this, 'update']); + $app->delete('/{id}', [$this, 'delete']); + + // Revisions + $app->get('/{id}/revisions', [$this, 'webhookRevisions']); + $app->get('/{id}/revisions/{offset}', [$this, 'oneWebhookRevision']); + + } + + /** + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function all(Request $request, Response $response) + { + $service = new WebhookService($this->container); + $responseData = $service->findAll( + $request->getQueryParams() + ); + + return $this->responseWithData($request, $response, $responseData); + } + + /** + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function create(Request $request, Response $response) + { + $this->validateRequestPayload($request); + $payload = $request->getParsedBody(); + if (isset($payload[0]) && is_array($payload[0])) { + return $this->batch($request, $response); + } + $service = new WebhookService($this->container); + $responseData = $service->create( + $payload, + $request->getQueryParams() + ); + + return $this->responseWithData($request, $response, $responseData); + } + + /** + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function read(Request $request, Response $response) + { + $service = new WebhookService($this->container); + $responseData = $service->findByIds( + $request->getAttribute('id'), + $request->getQueryParams() + ); + + return $this->responseWithData($request, $response, $responseData); + } + + /** + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function update(Request $request, Response $response) + { + $this->validateRequestPayload($request); + $service = new WebhookService($this->container); + + $payload = $request->getParsedBody(); + if (isset($payload[0]) && is_array($payload[0])) { + return $this->batch($request, $response); + } + + $id = $request->getAttribute('id'); + + if (strpos($id, ',') !== false) { + return $this->batch($request, $response); + } + + $responseData = $service->update( + $id, + $payload, + $request->getQueryParams() + ); + + return $this->responseWithData($request, $response, $responseData); + } + + /** + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function delete(Request $request, Response $response) + { + $service = new WebhookService($this->container); + + $id = $request->getAttribute('id'); + if (strpos($id, ',') !== false) { + return $this->batch($request, $response); + } + + $service->delete( + $id, + $request->getQueryParams() + ); + + return $this->responseWithData($request, $response, []); + } + + /** + * @param Request $request + * @param Response $response + * + * @return Response + * + * @throws \Exception + */ + protected function batch(Request $request, Response $response) + { + $service = new WebhookService($this->container); + + $payload = $request->getParsedBody(); + $params = $request->getQueryParams(); + + $responseData = null; + if ($request->isPost()) { + $responseData = $service->batchCreate($payload, $params); + } else if ($request->isPatch()) { + if ($request->getAttribute('id')) { + $ids = explode(',', $request->getAttribute('id')); + $responseData = $service->batchUpdateWithIds( $ids, $payload, $params); + } else { + $responseData = $service->batchUpdate($payload, $params); + } + } else if ($request->isDelete()) { + $ids = explode(',', $request->getAttribute('id')); + $service->batchDeleteWithIds($ids, $params); + } + + return $this->responseWithData($request, $response, $responseData); + } + + /** + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function webhookRevisions(Request $request, Response $response) + { + $service = new RevisionsService($this->container); + $responseData = $service->findAllByItem( + SchemaManager::COLLECTION_WEBHOOKS, + $request->getAttribute('id'), + $request->getQueryParams() + ); + + return $this->responseWithData($request, $response, $responseData); + } + + /** + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function oneWebhookRevision(Request $request, Response $response) + { + $service = new RevisionsService($this->container); + $responseData = $service->findOneByItemOffset( + SchemaManager::COLLECTION_WEBHOOKS, + $request->getAttribute('id'), + $request->getAttribute('offset'), + $request->getQueryParams() + ); + + return $this->responseWithData($request, $response, $responseData); + } + +} diff --git a/src/helpers/all.php b/src/helpers/all.php index a8b222822c..b52639b9c4 100644 --- a/src/helpers/all.php +++ b/src/helpers/all.php @@ -8,9 +8,13 @@ use Directus\Hook\Emitter; use Directus\Util\ArrayUtils; use Directus\Util\DateTimeUtils; +use Directus\Database\TableGateway\DirectusUserSessionsTableGateway; use Directus\Util\Installation\InstallerUtils; +use Directus\Services\WebhookService; +use Directus\Database\TableGateway\BaseTableGateway; use Directus\Util\JWTUtils; use Directus\Util\StringUtils; +use Directus\Services\UserSessionService; use Phinx\Db\Adapter\AdapterInterface; use RKA\Middleware\ProxyDetection; use Slim\Http\Cookies; @@ -19,6 +23,10 @@ use Slim\Http\RequestBody; use Slim\Http\UploadedFile; use Slim\Http\Uri; +use Directus\Authentication\Exception\InvalidTokenException; + + +const TOKEN_CIPHER_METHOD = 'aes-128-ctr'; require __DIR__ . '/constants.php'; require __DIR__ . '/app.php'; @@ -245,8 +253,20 @@ function get_api_project_from_request() 'check_proxy' => false, ]); - $authToken = get_request_authorization_token($request); - if (JWTUtils::isJWT($authToken)) { + if ($request->hasHeader('Authorization')) { + $authorizationHeader = $request->getHeader('Authorization'); + + // If there's multiple Authorization header, pick first, ignore the rest + if (is_array($authorizationHeader)) { + $authorizationHeader = array_shift($authorizationHeader); + } + + if (is_string($authorizationHeader) && preg_match("/Bearer\s+(.*)$/i", $authorizationHeader, $matches)) { + $authToken = $matches[1]; + } + } + + if(isset($authToken)){ $name = JWTUtils::getPayload($authToken, 'project'); } else { $name = get_request_project_name($request); @@ -267,11 +287,13 @@ function get_api_project_from_request() */ function get_request_authorization_token(Request $request) { - $authToken = null; + $response = []; if ($request->getParam('access_token')) { - $authToken = $request->getParam('access_token'); + $response['type'] = DirectusUserSessionsTableGateway::TOKEN_JWT; + $response['token'] = $request->getParam('access_token'); } elseif ($request->hasHeader('Php-Auth-User')) { + $response['type'] = DirectusUserSessionsTableGateway::TOKEN_JWT; $authUser = $request->getHeader('Php-Auth-User'); $authPassword = $request->getHeader('Php-Auth-Pw'); @@ -284,9 +306,11 @@ function get_request_authorization_token(Request $request) } if ($authUser && (empty($authPassword) || $authUser === $authPassword)) { - $authToken = $authUser; + $response['token'] = $authUser; } + } elseif ($request->hasHeader('Authorization')) { + $response['type'] = DirectusUserSessionsTableGateway::TOKEN_JWT; $authorizationHeader = $request->getHeader('Authorization'); // If there's multiple Authorization header, pick first, ignore the rest @@ -295,11 +319,97 @@ function get_request_authorization_token(Request $request) } if (is_string($authorizationHeader) && preg_match("/Bearer\s+(.*)$/i", $authorizationHeader, $matches)) { - $authToken = $matches[1]; + $response['token'] = $matches[1]; } + } elseif ($request->hasHeader('Cookie')) { + $response['type'] = DirectusUserSessionsTableGateway::TOKEN_COOKIE; + $authorizationHeader = $request->getCookieParam(get_project_session_cookie_name($request)); + $response['token'] = $authorizationHeader; } + return $response; + } +} + +if (!function_exists('get_project_session_cookie_name')) { + /** + * Returns the session cookie name of current project + * + * @param Request $request + * + * @return null|string + */ + function get_project_session_cookie_name($request) + { + $projectName = get_api_project_from_request($request); + return 'directus-'.$projectName.'-session'; + } +} - return $authToken; +if (!function_exists('get_static_token_based_on_type')) { + /** + * Returns the static token of users table from a encrypted token of sessions table + * + * @param Request $request + * + * @return null|string + */ + function get_static_token_based_on_type($tokenObject) + { + $accessToken = null; + if(!empty($tokenObject['token'])){ + switch($tokenObject['type']){ + case DirectusUserSessionsTableGateway::TOKEN_COOKIE : + $container = Application::getInstance()->getContainer(); + $decryptedToken = decrypt_static_token($tokenObject['token']); + $userSessionService = new UserSessionService($container); + $userSession = $userSessionService->find(['token' => $decryptedToken]); + if($userSession){ + $user = $container->get('auth')->getUserProvider()->find($userSession['user'])->toArray(); + $accessToken = $user['token']; + }else{ + throw new InvalidTokenException(); + } + break; + default : + $accessToken = $tokenObject['token']; + break; + } + } + return $accessToken; + } +} + +if (!function_exists('encrypt_static_token')) { + /** + * Returns the encrypted static token + * + * @param Request $request + * + * @return null|string + */ + function encrypt_static_token($token) + { + $enc_key = openssl_digest(php_uname(), 'SHA256', TRUE); + $enc_iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(TOKEN_CIPHER_METHOD)); + $cryptedToken = openssl_encrypt($token, TOKEN_CIPHER_METHOD, $enc_key, 0, $enc_iv) . "::" . bin2hex($enc_iv); + return $cryptedToken; + } +} + +if (!function_exists('decrypt_static_token')) { + /** + * Returns the decrypted static token + * + * @param Request $request + * + * @return null|string + */ + function decrypt_static_token($token) + { + list($cryptedToken, $enc_iv) = explode("::", $token); + $enc_key = openssl_digest(php_uname(), 'SHA256', TRUE); + $token = openssl_decrypt($cryptedToken,TOKEN_CIPHER_METHOD, $enc_key, 0, hex2bin($enc_iv)); + return $token; } } @@ -509,6 +619,38 @@ function register_extensions_hooks(Application $app) } } +if (!function_exists('register_webhooks')) { + /** + * Register all the hooks from the directus_webhooks table + * + * @param Application $app + */ + function register_webhooks(Application $app) + { + $app = Application::getInstance(); + BaseTableGateway::setContainer($app->getContainer()); + try{ + $webhook = new WebhookService($app->getContainer()); + $webhookData = $webhook->findAll(['status' => \Directus\Api\Routes\Webhook::STATUS_ACTIVE],false); + $result = []; + foreach($webhookData['data'] as $hook){ + $action = explode(":",$hook['directus_action']); + $result['hooks']['actions'][$action[0].".".$hook['collection'].":".$action[1]] = function ($data) use ($hook) { + $client = new \GuzzleHttp\Client(); + $response = []; + if($hook['http_action'] == WebhookService::HTTP_ACTION_POST){ + $response['form_params'] = ($data); + } + $client->request($hook['http_action'], $hook['url'], $response); + }; + } + register_hooks_list($app,$result); + }catch(\Exception $e){ + return true; + } + } +} + if (!function_exists('register_hooks_list')) { /** * Register an array of hooks (containing a list of actions and filters) diff --git a/src/helpers/app.php b/src/helpers/app.php index 82d3eebfe3..c82ea6d545 100644 --- a/src/helpers/app.php +++ b/src/helpers/app.php @@ -13,9 +13,10 @@ use Directus\Config\Context; use Directus\Config\Schema\Schema; use Directus\Config\Exception\UnknownProjectException; -use Directus\Config\Exception\InvalidProjectException; use Directus\Exception\Exception; use Directus\Exception\UnauthorizedException; +use Directus\Util\Installation\InstallerUtils; +use Directus\Util\StringUtils; use Slim\Http\Body; if (!function_exists('create_app')) { @@ -67,7 +68,7 @@ function create_app_with_project_name($basePath, $name, array $values = []) * * @throws Exception */ - function get_project_config($name = '_', $basePath = null) + function get_project_config($name, $basePath = null) { static $configs = []; @@ -75,14 +76,8 @@ function get_project_config($name = '_', $basePath = null) $basePath = get_app_base_path(); } - $configPath = $basePath . '/config'; - - if (!empty($name) && $name !== '_') { - $configFilePath = sprintf('%s/api.%s.php', $configPath, $name); - } else { - $configFilePath = $configPath . '/api.php'; - } - + $configFilePath = InstallerUtils::createConfigPath($basePath, $name); + if (isset($configs[$configFilePath])) { return $configs[$configFilePath]; } @@ -91,9 +86,6 @@ function get_project_config($name = '_', $basePath = null) $schema = Schema::get(); if (getenv("DIRECTUS_USE_ENV") === "1") { - if ($name !== "_") { - throw new InvalidProjectException(); - } $configFilePath = "__env__"; $configData = $schema->value(Context::from_env()); } else { @@ -124,6 +116,32 @@ function get_app_base_path() } } +if (!function_exists('scan_config_folder')) { + /** + * Scan config folder and return the php files (Project Configurations) + * + * @return string + */ + function scan_config_folder() + { + $projectNames = []; + $ignoreableFiles = ['api_sample.php','.DS_Store','..', '.']; + $scannedDirectory = array_values(array_diff(scandir(get_app_base_path().'/config'), $ignoreableFiles)); + if(!empty($scannedDirectory)){ + foreach($scannedDirectory as $fileName){ + $fileObject = explode(".",$fileName); + if (StringUtils::startsWith($fileName, '_')) { + continue; + } + if(end($fileObject) == "php" ){ + $projectNames[] = implode(".",$fileObject); + } + } + } + return $projectNames; + } +} + if (!function_exists('ping_route')) { /** * Returns a ping route @@ -241,9 +259,9 @@ function create_default_app($basePath, array $config = [], array $values = []) $app->add(new CorsMiddleware($app->getContainer(), true)); - $app->group('/server', function () { - create_ping_route($this); - }); + $app->get('/', \Directus\Api\Routes\Home::class); + $app->group('/server', \Directus\Api\Routes\Server::class); + create_install_route($app); return $app; diff --git a/src/helpers/cors.php b/src/helpers/cors.php index 0e25a69cc9..1c5760f4db 100644 --- a/src/helpers/cors.php +++ b/src/helpers/cors.php @@ -34,7 +34,7 @@ function cors_get_allowed_origin($allowedOrigins, $requestedOrigin) if (in_array($requestedOrigin, $allowedOrigins)) { $allowedOrigin = $requestedOrigin; } else if (in_array('*', $allowedOrigins)) { - $allowedOrigin = '*'; + $allowedOrigin = !empty($requestedOrigin) ? $requestedOrigin : '*'; } else { $allowedOrigin = null; } diff --git a/src/helpers/mail.php b/src/helpers/mail.php index 8fe2dc9e94..f921bb4f17 100644 --- a/src/helpers/mail.php +++ b/src/helpers/mail.php @@ -135,28 +135,6 @@ function parse_twig($viewPath, array $data) } } -if (!function_exists('send_reset_password_email')) { - /** - * Sends a new password email - * - * @param $user - * @param string $password - */ - function send_reset_password_email($user, $password) - { - $data = [ - 'new_password' => $password, - 'user_full_name' => get_user_full_name($user), - ]; - send_mail_with_template('reset-password.twig', $data, function (Message $message) use ($user) { - $message->setSubject( - sprintf('New Temporary Password: %s', get_directus_setting('project_name', '')) - ); - $message->setTo($user['email']); - }); - } -} - if (!function_exists('send_forgot_password_email')) { /** * Sends a new reset password email @@ -164,13 +142,13 @@ function send_reset_password_email($user, $password) * @param array $user * @param string $token */ - function send_forgot_password_email(array $user, $token) + function send_forgot_password_email(array $user, $url) { $data = [ - 'reset_token' => $token, + 'reset_url' => $url, 'user_full_name' => get_user_full_name($user), ]; - send_mail_with_template('forgot-password.twig', $data, function (Message $message) use ($user) { + send_mail_with_template('reset-password.twig', $data, function (Message $message) use ($user) { $message->setSubject( sprintf('Password Reset Request: %s', get_directus_setting('project_name', '')) ); diff --git a/src/helpers/settings.php b/src/helpers/settings.php index 8033860f03..ec77c9618c 100644 --- a/src/helpers/settings.php +++ b/src/helpers/settings.php @@ -156,16 +156,11 @@ function get_trusted_proxies() */ function get_project_info() { - $settings = get_directus_settings_by_keys(['project_name', 'logo']); - - if (array_get($settings, 'logo')) { - $settings['logo'] = get_project_logo_data(array_get($settings, 'logo')); - } - - array_rename($settings, [ - 'logo' => 'project_logo', - ]); - + $settings = get_directus_settings_by_keys(['project_name', 'project_logo','project_color','project_foreground','project_background','default_locale', 'telemetry']); + $settings['project_logo'] = array_get($settings, 'project_logo') ? get_project_logo_data(array_get($settings, 'project_logo')) : null; + $settings['project_foreground'] = array_get($settings, 'project_foreground') ? get_project_logo_data(array_get($settings, 'project_foreground')) : null; + $settings['project_background'] = array_get($settings, 'project_background') ? get_project_logo_data(array_get($settings, 'project_background')) : null; + $settings['telemetry'] = array_get($settings, 'telemetry') && $settings['telemetry'] ? true : false; return $settings; } } diff --git a/src/mail/base.twig b/src/mail/base.twig index ef223a78ae..4a528de9c9 100644 --- a/src/mail/base.twig +++ b/src/mail/base.twig @@ -9,25 +9,25 @@ a { border: none; text-decoration: none; - color: #4ba6de; + color: #2196f3; outline: none !important; - color: #999999 !important; + color: #2196f3 !important; } a:hover { - color: #666666 !important; + color: #2196f3 !important; } p { margin: 20px 0 20px 0; } - +
- +
- @@ -35,7 +35,7 @@
+ Directus
- @@ -43,10 +43,10 @@ -
+ {% block content %}{{ body|raw }}{% endblock %}
+ -
+ {% block footer %} {% include 'footer.twig' %} {% endblock %} diff --git a/src/mail/forgot-password.twig b/src/mail/forgot-password.twig deleted file mode 100644 index 1f3a41e912..0000000000 --- a/src/mail/forgot-password.twig +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.twig" %} -{% block content %} - -

Hey {{ user_full_name }},

- -

You requested to reset your password, here is your reset password link:

- -{% set reset_url = settings.project_url|trim('/') ~ '/' ~ api.project ~ '/auth/password/reset/' ~ reset_token %} -

{{ reset_url }}

- -

Love,
Directus

- -{% endblock %} diff --git a/src/mail/reset-password.twig b/src/mail/reset-password.twig index be9c8141ea..fde06da498 100644 --- a/src/mail/reset-password.twig +++ b/src/mail/reset-password.twig @@ -3,12 +3,10 @@

Hey {{ user_full_name }},

-

Here is a temporary password to access Directus:

+

You requested to reset your password, click here to reset your password:

-

{{ new_password }}

+

Reset my password

-

Once you log in, you can change your password via the User Settings menu.

- -

Love,
Directus

+

Love,
Directus

{% endblock %} diff --git a/src/web.php b/src/web.php index 5742c11309..15546d3e6b 100644 --- a/src/web.php +++ b/src/web.php @@ -8,16 +8,6 @@ require $basePath . '/vendor/autoload.php'; -// Creates a simple endpoint to test the server rewriting -// If the server responds "pong" it means the rewriting works -// NOTE: The API requires the default project to be configured to properly works -// It should work without the default project being configured -if (getenv("DIRECTUS_USE_ENV") !== "1") { - if (!file_exists($basePath . '/config/api.php')) { - return \Directus\create_default_app($basePath); - } -} - // Get Environment name $projectName = \Directus\get_api_project_from_request(); @@ -31,10 +21,10 @@ } else { $configData = $schema->value([]); } + return \Directus\create_unknown_project_app($basePath, $configData); } - $maintenanceFlagPath = \Directus\create_maintenanceflag_path($basePath); if (file_exists($maintenanceFlagPath)) { http_response_code(503); @@ -51,6 +41,9 @@ try { $app = \Directus\create_app_with_project_name($basePath, $projectName); } catch (ErrorException $e) { + if($e->getCode() == Directus\Config\Exception\UnknownProjectException::ERROR_CODE){ + return \Directus\create_unknown_project_app($basePath); + } http_response_code($e->getStatusCode()); header('Content-Type: application/json'); echo json_encode([ @@ -95,6 +88,7 @@ try { \Directus\register_global_hooks($app); \Directus\register_extensions_hooks($app); + \Directus\register_webhooks($app); } catch (ErrorException $e) { http_response_code($e->getStatusCode()); header('Content-Type: application/json'); @@ -107,7 +101,6 @@ exit; } - $app->getContainer()->get('hook_emitter')->run('application.boot', $app); // TODO: Implement a way to register middleware with a name @@ -118,6 +111,7 @@ // Ex: $app->add(['global', 'auth']); $middleware = [ 'table_gateway' => new \Directus\Application\Http\Middleware\TableGatewayMiddleware($app->getContainer()), + 'database_migration' => new \Directus\Application\Http\Middleware\DatabaseMigrationMiddleware($app->getContainer()), 'rate_limit_ip' => new \Directus\Application\Http\Middleware\IpRateLimitMiddleware($app->getContainer()), 'ip' => new RKA\Middleware\IpAddress(), 'proxy' => new \Directus\Application\Http\Middleware\ProxyMiddleware(), @@ -133,13 +127,11 @@ $app->add($middleware['rate_limit_ip']) ->add($middleware['proxy']) ->add($middleware['ip']) + ->add($middleware['database_migration']) ->add($middleware['cors']); $app->get('/', \Directus\Api\Routes\Home::class) ->add($middleware['rate_limit_user']) - ->add($middleware['auth_user']) - ->add($middleware['auth']) - ->add($middleware['auth_ignore_origin']) ->add($middleware['table_gateway']); $app->group('/projects', function () use ($middleware) { @@ -204,6 +196,7 @@ $this->group('/settings', \Directus\Api\Routes\Settings::class) ->add($middleware['rate_limit_user']) ->add($middleware['auth']) + ->add($middleware['database_migration']) ->add($middleware['table_gateway']); $this->group('/collections', \Directus\Api\Routes\Collections::class) ->add($middleware['rate_limit_user']) @@ -213,6 +206,10 @@ ->add($middleware['rate_limit_user']) ->add($middleware['auth']) ->add($middleware['table_gateway']); + $this->group('/webhooks', \Directus\Api\Routes\Webhook::class) + ->add($middleware['rate_limit_user']) + ->add($middleware['auth']) + ->add($middleware['table_gateway']); $this->group('/scim', function () { $this->group('/v2', \Directus\Api\Routes\ScimTwo::class); })->add($middleware['rate_limit_user']) @@ -271,26 +268,23 @@ ->add($middleware['rate_limit_user']) ->add($middleware['auth_user']) ->add($middleware['auth']) - ->add($middleware['auth_ignore_origin']) ->add($middleware['table_gateway']); $app->group('/layouts', \Directus\Api\Routes\Layouts::class) ->add($middleware['rate_limit_user']) ->add($middleware['auth_user']) ->add($middleware['auth']) - ->add($middleware['auth_ignore_origin']) ->add($middleware['table_gateway']); $app->group('/pages', \Directus\Api\Routes\Pages::class) ->add($middleware['rate_limit_user']) ->add($middleware['auth_user']) ->add($middleware['auth']) - ->add($middleware['auth_ignore_origin']) ->add($middleware['table_gateway']); + $app->group('/server', \Directus\Api\Routes\Server::class); $app->group('/types', \Directus\Api\Routes\Types::class) ->add($middleware['rate_limit_user']) ->add($middleware['auth_user']) ->add($middleware['auth']) - ->add($middleware['auth_ignore_origin']) ->add($middleware['table_gateway']); $app->add(new \Directus\Application\Http\Middleware\ResponseCacheMiddleware($app->getContainer()));