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 @@
-
+ |
{% block content %}{{ body|raw }}{% endblock %}
|
@@ -43,10 +43,10 @@
-
+ |
-
+ |
{% 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()));
| | | |