Merge branch 'master' into fix1551

This commit is contained in:
Jim LAURIE 2018-08-29 14:30:37 +02:00 committed by GitHub
commit 4d5a285cde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 277 additions and 179 deletions

View File

@ -21,11 +21,11 @@ Every user can send a feature request using the [issues](https://github.com/stra
## Repository Organization
We made the choice to use a monorepo design such as [React](https://github.com/facebook/react/tree/master/packages), [Babel](https://github.com/babel/babel/tree/master/packages), [Meteor](https://github.com/meteor/meteor/tree/devel/packages) or [Ember](https://github.com/emberjs/ember.js/tree/master/packages) do. It allows the community to easily maintain the whole ecosystem up-to-date and consistent.
The Babel team wrotes an excellent short post about [the pros and cons of the monorepo design](https://github.com/babel/babel/blob/master/doc/design/monorepo.md).
The Babel team wrote an excellent short post about [the pros and cons of the monorepo design](https://github.com/babel/babel/blob/master/doc/design/monorepo.md).
We will do our best to keep the master branch clean as possible, with tests passing all the times. However, it can happen that the master branch moves faster than the release cycle. To ensure to use the latest stable version, please refers to the [release on npm](https://www.npmjs.com/package/strapi).
We will do our best to keep the master branch as clean as possible, with tests passing all the times. However, it can happen that the master branch moves faster than the release cycle. To ensure you have the latest stable version, please refer to the [release on npm](https://www.npmjs.com/package/strapi).
If you send a pull request, please do it again the `master` branch. We are developing upcoming versions separately to ensure non-breaking changes from master to the latest stable major version.
If you send a pull request, please do it against the `master` branch. We are developing upcoming versions separately to ensure non-breaking changes from master to the latest stable major version.
***
@ -53,7 +53,7 @@ cd strapi
**Two setup are available... with or without the front-end builds.**
Without the front-end builds, you won't be able to access to the administration panel via http://localhost:1337/admin, you'll have to run the administration separately and access it through http://localhost:4000/admin.
Without the front-end builds, you won't be able to access the administration panel via http://localhost:1337/admin. You'll have to run the administration separately and access it through http://localhost:4000/admin.
<br>

View File

@ -15,10 +15,10 @@ The most advanced open-source Content Management Framework to build powerful API
{% endcenter %}
## v3@alpha.13 is available!
## v3@alpha.14 is available!
We've been working on a major update for Strapi during the past months, rewriting the core framework and the dashboard.
This documentation is only related to Strapi v3@alpha.13 ([v1 documentation is still available](http://strapi.io/documentation/1.x.x)).
This documentation is only related to Strapi v3@alpha.14 ([v1 documentation is still available](http://strapi.io/documentation/1.x.x)).
**[Get Started](getting-started/installation.md)**<br />
Learn how to install Strapi and start developing your API.

View File

@ -335,7 +335,8 @@ Most of the application's configurations are defined by environment. It means th
- `host` (string): Host name. Default value: `localhost`.
- `port` (integer): Port on which the server should be running. Default value: `1337`.
- `autoReload` (boolean): Enable or disabled server reload on files update. Default value: depends on the environment.
- `autoReload`
- `enabled` (boolean): Enable or disabled server reload on files update. Default value: depends on the environment.
- `proxy`
- `enabled` (boolean): Enable proxy support such as Apache or Nginx. Default value: `false`.
- `ssl` (boolean): Enable proxy SSL support

View File

@ -190,6 +190,140 @@ The User object is available to successfully authenticated requests.
```
## Add a new provider
To add a new provider on strapi, you will need to perform changes onto the following files:
```
packages/strapi-plugin-users-permissions/services/Providers.js
packages/strapi-plugin-users-permissions/config/functions/bootstrap.js
packages/strapi-plugin-users-permissions/admin/src/components/PopUpForm/index.js
packages/strapi-plugin-users-permissions/admin/src/translations/en.json
```
We will go step by step.
### Configure your Provider request
First, we need to configure our new provider onto `Provider.js` file.
Jump onto the `getProfile` function, you will see the list of currently available providers in the form of a `switch...case`.
As you can see, `getProfile` take three params:
1. provider :: The name of the used provider as a string.
2. query :: The query is the result of the provider callback.
3. callback :: The callback function who will continue the internal strapi login logic.
Let's take the `discord` one as an example since it's not the easier, it should cover most of the case you may encounter trying to implement your own provider.
#### Configure your oauth generic information
```js
case 'discord': {
const discord = new Purest({
provider: 'discord',
config: {
'discord': {
'https://discordapp.com/api/': {
'__domain': {
'auth': {
'auth': {'bearer': '[0]'}
}
},
'{endpoint}': {
'__path': {
'alias': '__default'
}
}
}
}
}
});
```
So here, you can see that we use a module called `Purest`. This module gives us with a generic way to interact with the REST API.
To understand each value usage, and the templating syntax, I invite you to read the [Official Purest Documentation](https://github.com/simov/purest/tree/2.x)
You may also want to take a look onto the numerous already made configurations [here](https://github.com/simov/purest-providers/blob/master/config/providers.json).
#### Retrieve your user informations:
```js
discord.query().get('users/@me').auth(access_token).request((err, res, body) => {
if (err) {
callback(err);
} else {
// Combine username and discriminator because discord username is not unique
var username = `${body.username}#${body.discriminator}`;
callback(null, {
username: username,
email: body.email
});
}
});
break;
}
```
Here is the next part of our switch. Now that we have properly configured our provider, we want to use it to retrieve user information.
Here you see the real power of `purest`, you can simply make a get request on the desired URL, using the `access_token` from the `query` parameter to authenticate.
That way, you should be able to retrieve the user info you need.
Now, you can simply call the `callback` function with the username and email of your user. That way, strapi will be able to retrieve your user from the database and log you in.
#### Configure the new provider model onto database
Now, we need to configure our 'model' for our new provider. That way, our settings can be stored in the database, and managed from the admin panel.
Into: `packages/strapi-plugin-users-permissions/config/functions/bootstrap.js`
Simply add the fields your provider need into the `grantConfig` object.
For our discord provider it will look like:
```js
discord: {
enabled: false, // make this provider disabled by default
icon: 'comments', // The icon to use on the UI
key: '', // our provider app id (leave it blank, you will fill it with the content manager)
secret: '', // our provider secret key (leave it blank, you will fill it with the content manager)
callback: '/auth/discord/callback', // the callback endpoint of our provider
scope: [ // the scope that we need from our user to retrieve infos
'identify',
'email'
]
},
```
You have already done the hard part, now, we simply need to make our new provider available from the front side of our application. So let's do it!
<!-- #### Tests -->
<!-- TODO Add documentation about how to configure unit test for the new provider -->
### Configure frontend for your new provider
First, let's edit: `packages/strapi-plugin-users-permissions/admin/src/components/PopUpForm/index.js`
As for backend, we have a `switch...case` where we need to put our new provider info.
```js
case 'discord':
return `${strapi.backendURL}/connect/discord/callback`;
```
Add the corresponding translation into: `packages/strapi-plugin-users-permissions/admin/src/translations/en.json`
```js
"PopUpForm.Providers.discord.providerConfig.redirectURL": "The redirect URL to add in your Discord application configurations",
````
These two change will set up the popup message who appear on the UI when we will configure our new provider.
That's it, now you should be able to use your new provider.
## Email templates
[See the documentation on GitHub](https://github.com/strapi/strapi/blob/master/packages/strapi-plugin-users-permissions/docs/email-templates.md)

View File

@ -22,7 +22,7 @@ module.exports = async (ctx, next) => {
In this example, we are verifying that a session is open. If it is the case, we call the `next()` method that will execute the next policy or controller's action. Otherwise, a 401 error is returned.
> Note: You can access to any controllers, services or models thanks to the global variable `strapi` in a policy.
> Note: You can access any controllers, services, or models by using the global variable `strapi` in a policy.
## Usage

View File

@ -13,6 +13,7 @@ The plugin exposes a single route `POST /upload` to upload one or multiple files
**Parameters**
- `files`: The file(s) to upload. The value(s) can be a Buffer or Stream.
- `path`: (optional): The folder where the file(s) will be uploaded to (only supported on strapi-upload-aws-s3 now).
- `refId`: (optional): The ID of the entry which the file(s) will be linked to.
- `ref`: (optional): The name of the model which the file(s) will be linked to (see more below).
- `source`: (optional): The name of the plugin where the model is located.
@ -111,6 +112,7 @@ Let's say that you want to have a `User` model provided by the plugin `Users & P
```js
{
"files": "...", // Buffer or stream of file(s)
"path": "user/avatar", // Uploading folder of file(s).
"refId": "5a993616b8e66660e8baf45c", // User's Id.
"ref": "user", // Model name.
"source": "users-permissions", // Plugin name.

View File

@ -346,7 +346,7 @@ const send = require('koa-send');
module.exports = {
autoReload: async ctx => {
ctx.send({ autoReload: _.get(strapi.config.environments, 'development.server.autoReload', false) });
ctx.send({ autoReload: _.get(strapi.config.currentEnvironment, 'server.autoReload', { enabled: false }) });
}
}
```
@ -374,8 +374,8 @@ import request from 'utils/request';
const shouldRenderCompo = (plugin) => new Promise((resolve, request) => {
request('/my-plugin/autoReload')
.then(response => {
// If autoReload is enabled the response is `{ autoReload: true }`
plugin.preventComponentRendering = !response.autoReload;
// If autoReload is enabled the response is `{ autoReload: { enabled: true } }`
plugin.preventComponentRendering = !response.autoReload.enabled;
// Set the BlockerComponent props
plugin.blockerComponentProps = {
blockerComponentTitle: 'my-plugin.blocker.title',
@ -407,8 +407,8 @@ import MyCustomBlockerComponent from 'components/MyCustomBlockerComponent';
const shouldRenderCompo = (plugin) => new Promise((resolve, request) => {
request('/my-plugin/autoReload')
.then(response => {
// If autoReload is enabled the response is `{ autoReload: true }`
plugin.preventComponentRendering = !response.autoReload;
// If autoReload is enabled the response is `{ autoReload: { enabled: true } }`
plugin.preventComponentRendering = !response.autoReload.enabled;
// Tell which component to be rendered instead
plugin.blockerComponent = MyCustomBlockerComponent;

View File

@ -59,7 +59,7 @@ const renderIde = () => (
&nbsp;"port": 1337,
<br />
<span style={{ color: '#006EE7'}}>
&nbsp;"autoReload": true,
&nbsp;"autoReload": &#123; enabled: true &#125;
</span>
<br />
&nbsp;"proxi": &#123;

View File

@ -38,6 +38,10 @@ class InputFile extends React.Component {
handleChange = ({ target }) => this.addFilesToProps(target.files);
addFilesToProps = (files) => {
if (files.length === 0) {
return;
}
const initAcc = this.props.multiple ? cloneDeep(this.props.value) : {};
const value = Object.keys(files).reduce((acc, current) => {

View File

@ -23,7 +23,7 @@ const validateInput = (value, inputValidations = {}, type = 'text') => {
}
break;
case 'maxLength':
if (value.length > validationValue) {
if (value && value.length > validationValue) {
errors.push({ id: 'components.Input.error.validation.maxLength' });
}
break;
@ -33,12 +33,12 @@ const validateInput = (value, inputValidations = {}, type = 'text') => {
}
break;
case 'minLength':
if (value.length < validationValue) {
if (!value || value.length < validationValue) {
errors.push({ id: 'components.Input.error.validation.minLength' });
}
break;
case 'required':
if (value.length === 0) {
if (value == null || value.length === 0) {
errors.push({ id: 'components.Input.error.validation.required' });
}
break;

View File

@ -190,7 +190,7 @@ module.exports = {
autoReload: async ctx => {
ctx.send({
autoReload: _.get(strapi.config.environments, 'development.server.autoReload', false),
autoReload: _.get(strapi.config.currentEnvironment, 'server.autoReload', { enabled: false })
});
},

View File

@ -3,7 +3,7 @@ import request from 'utils/request';
const shouldRenderCompo = (plugin) => new Promise((resolve, reject) => {
request('/settings-manager/autoReload')
.then(response => {
plugin.preventComponentRendering = !response.autoReload;
plugin.preventComponentRendering = !response.autoReload.enabled;
plugin.blockerComponentProps = {
blockerComponentTitle: 'components.AutoReloadBlocker.header',
blockerComponentDescription: 'components.AutoReloadBlocker.description',

View File

@ -334,8 +334,8 @@ module.exports = {
autoReload: async ctx => {
ctx.send({
autoReload: _.get(strapi.config.environments, 'development.server.autoReload', false),
environment: strapi.config.environment,
autoReload: _.get(strapi.config.currentEnvironment, 'server.autoReload', { enabled: false }),
environment: strapi.config.environment
});
}
};

View File

@ -24,7 +24,7 @@ module.exports = {
}
// Extract optional relational data.
const { refId, ref, source, field } = ctx.request.body.fields;
const { refId, ref, source, field, path } = ctx.request.body.fields;
const { files = {} } = ctx.request.body.files;
if (_.isEmpty(files)) {
@ -50,6 +50,13 @@ module.exports = {
});
}
// Update uploading folder path for the file.
if (path) {
Object.assign(file, {
path
});
}
return file;
});

View File

@ -138,7 +138,7 @@ module.exports = async cb => {
},
response_email: '',
object: 'Account confirmation',
message: `<p>Thank you to register!</p>
message: `<p>Thank you for registering!</p>
<p>You have to confirm your email address. Please click on the link below.</p>

View File

@ -70,10 +70,12 @@ module.exports = {
upload: (file) => {
return new Promise((resolve, reject) => {
// upload file on S3 bucket
const path = file.path ? `${file.path}/` : '';
S3.upload({
Key: `${file.hash}${file.ext}`,
Key: `${path}${file.hash}${file.ext}`,
Body: new Buffer(file.buffer, 'binary'),
ACL: 'public-read'
ACL: 'public-read',
ContentType: file.mime,
}, (err, data) => {
if (err) {
return reject(err);
@ -89,12 +91,9 @@ module.exports = {
delete: (file) => {
return new Promise((resolve, reject) => {
// delete file on S3 bucket
const path = file.path ? `${file.path}/` : '';
S3.deleteObjects({
Delete: {
Objects: [{
Key: `${file.hash}${file.ext}`
}]
}
Key: `${path}${file.hash}${file.ext}`
}, (err, data) => {
if (err) {
return reject(err);

View File

@ -2,62 +2,61 @@
// Dependencies.
const path = require('path');
const fs = require('fs');
const fs = require('fs-extra');
const _ = require('lodash');
module.exports = function() {
return new Promise((resolve, reject) => {
const folder = ((url = _.get(strapi.config.currentEnvironment.server, 'admin.path', 'admin')) =>
url[0] === '/' ? url.substring(1) : url)().replace(/\/$/, '');
module.exports = async function() {
const folder = ((url = _.get(strapi.config.currentEnvironment.server, 'admin.path', 'admin')) =>
url[0] === '/' ? url.substring(1) : url)().replace(/\/$/, '');
const configuratePlugin = (acc, current, source, name) => {
switch (source) {
case 'host': {
const host =
_.get(this.config.environments[current].server, 'admin.build.host').replace(/\/$/, '') || '/';
const configuratePlugin = (acc, current, source, name) => {
switch (source) {
case 'host': {
const host =
_.get(this.config.environments[current].server, 'admin.build.host').replace(/\/$/, '') || '/';
if (!host) {
throw new Error(`You can't use \`remote\` as a source without set the \`host\` configuration.`);
}
const folder = _.get(this.config.environments[current].server, 'admin.build.plugins.folder', null);
if (_.isString(folder)) {
const cleanFolder = folder[0] === '/' ? folder.substring(1) : folder;
return `/${host}/${cleanFolder}/${name}/main.js`.replace('//', '/');
}
return `/${host}/${name}/main.js`.replace('//', '/');
if (!host) {
throw new Error(`You can't use \`remote\` as a source without set the \`host\` configuration.`);
}
case 'custom':
if (!_.isEmpty(_.get(this.plugins[name].config, `sources.${current}`, {}))) {
return (acc[current] = this.plugins[name].config.sources[current]);
}
throw new Error(
`You have to define the source URL for each environment in \`./plugins/**/config/sources.json\``,
);
case 'backend': {
const backend = _.get(
this.config.environments[current],
'server.admin.build.backend',
`http://${this.config.environments[current].server.host}:${
this.config.environments[current].server.port
}`,
).replace(/\/$/, '');
const folder = _.get(this.config.environments[current].server, 'admin.build.plugins.folder', null);
return `${backend}/${folder.replace(/\/$/, '')}/${name}/main.js`;
if (_.isString(folder)) {
const cleanFolder = folder[0] === '/' ? folder.substring(1) : folder;
return `/${host}/${cleanFolder}/${name}/main.js`.replace('//', '/');
}
default:
return `/${name}/main.js`;
return `/${host}/${name}/main.js`.replace('//', '/');
}
};
case 'custom':
if (!_.isEmpty(_.get(this.plugins[name].config, `sources.${current}`, {}))) {
return (acc[current] = this.plugins[name].config.sources[current]);
}
const sourcePath =
this.config.environment !== 'test'
? path.resolve(this.config.appPath, 'admin', 'admin', 'src', 'config', 'plugins.json')
: path.resolve(
throw new Error(
`You have to define the source URL for each environment in \`./plugins/**/config/sources.json\``,
);
case 'backend': {
const backend = _.get(
this.config.environments[current],
'server.admin.build.backend',
`http://${this.config.environments[current].server.host}:${
this.config.environments[current].server.port
}`,
).replace(/\/$/, '');
return `${backend}/${folder.replace(/\/$/, '')}/${name}/main.js`;
}
default:
return `/${name}/main.js`;
}
};
const sourcePath =
this.config.environment !== 'test'
? path.resolve(this.config.appPath, 'admin', 'admin', 'src', 'config', 'plugins.json')
: path.resolve(
this.config.appPath,
'packages',
'strapi-admin',
@ -66,10 +65,10 @@ module.exports = function() {
'config',
'plugins.json',
);
const buildPath =
this.config.environment !== 'test'
? path.resolve(this.config.appPath, 'admin', 'admin', 'build', 'config', 'plugins.json')
: path.resolve(
const buildPath =
this.config.environment !== 'test'
? path.resolve(this.config.appPath, 'admin', 'admin', 'build', 'config', 'plugins.json')
: path.resolve(
this.config.appPath,
'packages',
'strapi-admin',
@ -79,110 +78,55 @@ module.exports = function() {
'plugins.json',
);
try {
fs.access(path.resolve(this.config.appPath, 'admin', 'admin'), err => {
if (err && err.code !== 'ENOENT') {
return reject(err);
}
const isAdmin = await fs.pathExists(path.resolve(this.config.appPath, 'admin', 'admin'));
if (!isAdmin) return;
// No admin.
if (err && err.code === 'ENOENT') {
return resolve();
}
// arrange system directories
await Promise.all([
fs.remove(sourcePath),
// Try to access to path.
fs.access(sourcePath, err => {
if (err && err.code !== 'ENOENT') {
this.log.error(`Impossible to access to ${sourcePath}`);
(async () => {
const existBuildPath = await fs.pathExists(buildPath);
if (existBuildPath) {
await fs.remove(buildPath);
} else {
await fs.ensureDir(path.resolve(buildPath, '..', '..'));
}
})(),
return reject(err);
}
// Create `./config` folder
fs.ensureDir(path.resolve(buildPath, '..')),
]);
if (!err) {
// Delete source file.
fs.unlinkSync(sourcePath);
}
// Create `plugins.json` file.
// Don't inject the plugins without an Admin
const existingPlugins = await Object.keys(this.plugins).filter(plugin =>
fs.pathExists(path.resolve(this.config.appPath, 'plugins', plugin, 'admin', 'src', 'containers', 'App')),
);
// Try to access to path.
fs.access(buildPath, err => {
if (err && err.code !== 'ENOENT') {
this.log.error(`Impossible to access to ${buildPath}`);
const existingPluginsInfo = existingPlugins.map(id => ({
id,
source: Object.keys(this.config.environments).reduce((acc, current) => {
const source = _.get(this.config.environments[current].server, 'admin.build.plugins.source', 'default');
return reject(err);
}
if (_.isString(source)) {
acc[current] = configuratePlugin(acc, current, source, id);
} else if (_.isOject(source)) {
acc[current] = configuratePlugin(acc, current, source[current], id);
}
if (!err) {
// Delete build file.
fs.unlinkSync(buildPath);
}
return acc;
}, {}),
}));
if (err && err.code === 'ENOENT') {
try {
fs.accessSync(path.resolve(buildPath, '..', '..'));
} catch (err) {
if (err && err.code !== 'ENOENT') {
return reject(err);
}
fs.mkdirSync(path.resolve(buildPath, '..', '..'));
}
}
// Create `./config` folder
try {
fs.accessSync(path.resolve(buildPath, '..'));
} catch (err) {
if (err && err.code !== 'ENOENT') {
return reject(err);
}
fs.mkdirSync(path.resolve(buildPath, '..'));
}
// Create `plugins.json` file.
// Don't inject the plugins without an Admin
const data = Object.keys(this.plugins)
.filter(plugin => {
let hasAdminFolder;
try {
fs.accessSync(
path.resolve(this.config.appPath, 'plugins', plugin, 'admin', 'src', 'containers', 'App'),
);
hasAdminFolder = true;
} catch (err) {
hasAdminFolder = false;
}
return hasAdminFolder;
})
.map(name => ({
id: name,
source: Object.keys(this.config.environments).reduce((acc, current) => {
const source = _.get(
this.config.environments[current].server,
'admin.build.plugins.source',
'default',
);
if (_.isString(source)) {
acc[current] = configuratePlugin(acc, current, source, name);
} else if (_.isOject(source)) {
acc[current] = configuratePlugin(acc, current, source[current], name);
}
return acc;
}, {}),
}));
fs.writeFileSync(sourcePath, JSON.stringify(data, null, 2), 'utf8');
fs.writeFileSync(buildPath, JSON.stringify(data), 'utf8');
resolve();
});
});
});
} catch (e) {
reject(e);
}
});
await Promise.all([
fs.writeJSON(sourcePath, existingPluginsInfo, {
spaces: 2,
encoding: 'utf8',
}),
fs.writeJSON(buildPath, existingPluginsInfo, {
spaces: 2,
encoding: 'utf8',
}),
]);
};

View File

@ -34,6 +34,7 @@
"boom": "^5.2.0",
"cheerio": "^1.0.0-rc.2",
"delegates": "^1.0.0",
"fs-extra": "^7.0.0",
"glob": "^7.1.2",
"kcors": "^2.2.0",
"koa": "^2.1.0",
@ -91,4 +92,4 @@
},
"preferGlobal": true,
"license": "MIT"
}
}